在多线程与高并发编程之基础知识(上)一文中,对于线程的一些概念性的基础知识进行阐述,但是,实际开发中,更需要关注一些多线程编程面临的问题和挑战,多线程编程会面临哪些问题,这些问题又是如何产生的以及该如何解决这些问题等等,这些将是本文需要主要讲解的内容,在进行主要内容的叙述之前,先介绍一些相关的基础知识,并引出多线程编程中问题的产生原因,最后,再对实际开发中多线程编程问题进行描述、分析以及解决;
Java线程的实现:Java线程模型是基于操作系统原生线程模型来实现的;
线程模型只对线程的并发规模和操作成本产生影响,对Java程序的编写和运行过程来说,并没有什么不同;
时分形式是现代操作系统采用的基本线程调度形式,操作系统将CPU资源分为一个个的时间片,并分配给线程,线程使用获取的时间片执行任务,时间片使用完之后,操作系统进行线程调度,其他获得时间片的线程开始执行;那么,一个线程能够分配得到的时间片的多少决定了线程使用多少的处理器资源,线程优先级则是决定线程可以获得多或少的处理器资源的线程属性;
可以通过设置线程的优先级,使得线程获得处理器执行时间的长短有所不同,但采用这种方式来实现线程获取处理器执行时间的长短并不可靠(因为系统的优先级和Java中的优先级不是一一对应的,有可能Java中多个线程优先级对应于系统中同一个优先级);Java中有10个线程优先级,从1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY),默认优先级为5;
因此,程序的正确性不能够依赖线程优先级的高低来判断;
线程调度是指系统为线程分配处理器使用权的过程;主要调度方式有:抢占式线程调度、协同式线程调度;
每个线程由系统来分配执行时间,线程的切换不由线程本身决定;Java默认使用的线程调度方式是抢占式线程调度;我们可以通过Thread.yield()使当前正在执行的线程让出执行时间,但是,却没有办法使线程去获取执行时间;
每个线程的执行时间由线程本身来控制,线程执行完任务后主动通知系统,切换到另一个线程上;
协同式的优点:实现简单,可以通过对线程的切换控制避免线程安全问题;
协同式的缺点:一旦当前线程出现问题,将有可能影响到其他线程的执行,最终可能导致系统崩溃;
抢占式的优点:一个线程出现问题不会影响到其他线程的执行(线程的执行时间是由系统分配的,因此,系统可以将处理器执行时间分配给其他线程从而避免一个线程出现故障导致整个系统崩溃的现象发生);
在Java中,线程的调度策略主要是抢占式调度策略,正是因为抢占式调度策略,导致多线程程序执行过程中,实际的运行过程与我们逻辑上理解的顺序存在较大的区别,也就是多线程程序的执行具有不确定性,从而会导致一些线程安全性问题的发生;那么,什么是线程安全呢?
简单来说,线程安全就是对于多个线程并发执行的操作不需要进行任何外部的控制,也不需要进行任何的协调,就能够保证程序的执行结果与开发人员的预期结果保持一致,那么这个多线程程序就是线程安全的;
注意:
线程安全问题一定是基于多个线程之间存在访问共享数据这一前提下的;如果多个线程之间不会访问同一个变量,那么就不存在线程安全的问题;
线程安全这一概念并不仅仅分为线程安全和非线程安全,按照线程安全的强弱程度可以将各种共享变量的操作分为:不可变、绝对线程安全、相对线程安全、线程兼容以及线程对立这五种情况;
介绍了线程的调度原理之后,其实可以分析出线程安全问题的起因在于多线程的执行顺序具有不确定性,那么当多个线程同时操作一份资源就不出现意想不到的情况,而编译器和处理器会对执行的指令进行重排序,这些因素导致了线程安全问题;
那么,在实际开发中,我们一般需要解决的都是上述的相对线程安全以及线程兼容这两种线程安全性问题;那么,对于这两类问题,又可以细分为可见性、原子性以及有序性这三类问题;这里暂且先不进行细分,就线程安全问题,我们给出常用解决措施;
下面结合具体的代码来看一下使用多线程编程时可能出现的线程安全问题:
package com.thread;
public class ThreadSafe implements Runnable{
//静态变量,所有对象共享
private static int count = 0;
@Override
public void run() {
for(int i = 0 ; i < 100 ; i++){
count();
}
}
public void count(){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {
ThreadSafe threadSafe1 = new ThreadSafe();
ThreadSafe threadSafe2 = new ThreadSafe();
Thread thread1 = new Thread(threadSafe1);
Thread thread2 = new Thread(threadSafe2);
thread1.start();
thread2.start();
Thread.currentThread().sleep(1000);
System.out.println(count);
}
}
运行结果:
这一段代码的目的是开启两个线程对同一个变量分别进行100次的累加,按照正常的逻辑(串行化执行),累加后的结果应该为200,但是实际输出的结果却是190,显然这和我们的预期结果不同,这就是线程安全问题;我们分析一下,为什么会出现这样的情况,之前提到过,多线程执行的时候代码执行的顺序具有不确定性,那么就可能出现,线程1(thread1)在获取到count的值之后,CPU执行权被分配给了线程2(thread2),线程2获取到的值与线程1获取到的相同,那么两个线程累加操作执行后,相当于只累加来一次,这样就会导致线程不安全问题产生;那么,如何解决这个问题,我们可以利用Java中的synchronized关键字对线程体进行同步,代码如下:
package com.thread;
public class ThreadSafeTwo implements Runnable{
//静态变量,所有对象共享
private static int count = 0;
@Override
public void run() {
//这里对线程体进行同步
synchronized(ThreadSafeTwo.class){
for(int i = 0 ; i < 100 ; i++){
count();
}
}
}
public void count(){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {
ThreadSafeTwo threadSafe = new ThreadSafeTwo();
Thread thread1 = new Thread(threadSafe);
Thread thread2 = new Thread(threadSafe);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
}
同步处理后代码执行的结果如下:
显然,经过同步后的代码,就可以保证多线程并发执行的情况下,结果依然符合预期结果;关于synchronized关键字的实现原理将会另起一文进行分析,下面我们看一下,synchronized关键字的使用方式有哪些?
线程安全的单例模式实现
package com.thread;
public class SingleTonThreadSafe {
//属性私有化,volatile实现内存可见性、禁止指令重排序
private volatile static SingleTonThreadSafe singleTonThreadSafe = null;
//无参构造函数私有化
private SingleTonThreadSafe(){}
//静态方法外部使用,获取对象实例
public static SingleTonThreadSafe getInstance(){
//第一次判断,避免不必要的加锁
if(singleTonThreadSafe == null){
//同步实例化代码块
synchronized(SingleTonThreadSafe.class){
//再次检测,避免其它线程已经实例化
if(singleTonThreadSafe == null){
//实例化,其他线程立即可见
singleTonThreadSafe = new SingleTonThreadSafe();
}
}
}
return singleTonThreadSafe;
}
}
如果你的多线程程序仅仅是每个线程独立完成各自的任务,相互之间并没有交互和协作,那么,你的程序是无法发挥出多线程的优势的,只有有交互的多线程程序才是有意义的程序,否则,还不如使用单线程执行多个方法实现程序来的简单、易懂、有效!
那么,线程间进行交互通信的手段有哪些呢?下面,将给出常用的多线程通信的实现手段以及相应的代码示例,并结合具体的代码进行分析,对其中需要注意的地方进行突出提示;
我们先看这样一个场景:线程A修改了对象O的值,线程B感知到对象O的变化,执行相应的操作,这样就是一个线程间交互的场景;可以看出,这种方式,相当于线程A是发送了消息,线程B接收到消息,进行后续操作,是不是很像生产者与消费者的关系?我们都知道,生产者与消费者模式可以实现解耦,使得程序结构上具备伸缩性;那么Java中如何实现这种功能呢?
一种简单的方式是,线程B每隔一段时间就轮询对象O是否发生变化,如果发生变化,就结束轮询,执行后续操作;
但是,这种方式不能保证对象O的变更及时被线程B感知,同时,不断地轮询也会造成较大的开销;分析这些问题的症结在哪?其实,可以发现状态的感知是拉取的,而不是推送的,因此才会导致这样的问题产生;
那么,我们就会思考,如何将拉取变为推送来实现这样的功能呢?
这就引出了Java内置的经典的等待/通知机制,通过查看Object类的源码发现,该类中有三个方法,我们一般不会使用,但是在多线程编程中,这三个方法却是能够大放异彩的!那就是wait()/notify()/notifyAll();
/**
* 调用此方法会导致当前线程进入等待状态直到其它线程调用同一对象的notify()或者notifyAll()方法
* 当前线程必须拥有对象O的监视器,调用了对象O的此方法会导致当前线程释放已占有的监视器,并且等待
* 其它线程对象O的notify()或者notifyAll()方法,当其它线程执行了这两个方法中的一个之后,并且
* 当前线程获取到处理器执行权,就可以尝试获取监视器,进而继续后续操作的执行
* 推荐使用方式:
* synchronized (obj) {
* while (<condition does not hold>)
* obj.wait();
* ... // Perform action appropriate to condition
* }
* @throws IllegalMonitorStateException 如果当前线程没有获取到对象O的监视器时,抛出异常
* @throws InterruptedException 如果在调用了此方法之后,其他线程调用notify()或者notifyAll()
* 方法之前,线程被中断,则会清除中断标志并抛出异常
*/
public final void wait() throws InterruptedException {
wait(0);
}
/**
* 唤醒等待在对象O的监视器上的一个线程,如果多个线程等待在对象O的监视器上,那么将会选择其中的一个进行唤醒
* 被唤醒的线程只有在当前线程释放锁之后才能够继续执行.
* 被唤醒的线程将会与其他线程一同竞争对象O的监视器锁
* 这个方法必须在拥有对象O的监视器的线程中进行调用
* 同一个时刻,只能有一个线程拥有该对象的监视器
* @throws IllegalMonitorStateException 如果当前线程没有获取到对象O的监视器时,抛出异常
*/
public final native void notify();
/**
* 唤醒等待在对象O的监视器上的所有线程
* 被唤醒的线程只有在当前线程释放锁之后才能够继续执行.
* 被唤醒的线程将会与其他线程一同竞争对象O的监视器锁
* 这个方法必须在拥有对象O的监视器的线程中进行调用
* 同一个时刻,只能有一个线程拥有该对象的监视器
* @throws IllegalMonitorStateException 如果当前线程没有获取到对象O的监视器时,抛出异常
*/
public final native void notifyAll();
下面看一下如何通过这三个方法实现经典的等待通知机制吧!
按照JDK中推荐的使用方式实现了等待通知样例代码如下:
package com.thread;
public class WaitAndNotify {
//轮询标志位
private static boolean stop = false;
//监视器对应的对象
private static Object monitor = new Object();
//等待线程
static class WaitThread implements Runnable{
@Override
public void run() {
synchronized(monitor){
//循环检测标志位是否变更
while(!stop){
try {
//标志位未变更,进行等待
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒后获取到对象的监视器之后执行的代码
System.out.println("Thread "+Thread.currentThread().getName()+" is awakened at first time");
stop = false;
}
//休眠1秒之后,线程角色转换为唤醒线程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//与上述代码相反的逻辑
synchronized(monitor){
while(stop){
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
monitor.notify();
stop = true;
System.out.println("Thread "+ Thread.currentThread().getName()+" notifies the waitted thread at first time");
}
}
}
//通知线程
static class NotifyThread implements Runnable{
@Override
public void run() {
synchronized (monitor){
while(stop){
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
stop = true;
monitor.notify();
System.out.println("Thread "+ Thread.currentThread().getName()+" notifies the waitted thread at first time");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (monitor){
while(!stop){
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread "+Thread.currentThread().getName()+" is awakened at first time");
}
}
}
public static void main(String[] args){
Thread waitThread = new Thread(new WaitThread());
waitThread.setName("waitThread");
Thread notifyThread = new Thread(new NotifyThread());
notifyThread.setName("notifyThread");
waitThread.start();
notifyThread.start();
}
}
通过上述代码,可以提炼出等待通知机制的经典模式:
等待方实现步骤:
对应的伪代码:
synchronized(obj){
while(condition不满足){
obj.wait();
}
//后续操作
}
通知方实现步骤:
对应的伪代码:
synchronized(obj){
while(condition不满足){
obj.wait();
}
更新condition
obj.notify();
//后续操作
}
基于等待通知机制,我们可以很容易地写出生产者消费者模式的代码,下面给出一个实现样例代码:
package com.thread;
public class ProducerAndConsumer {
//商品库存
private static int storeMount = 0;
//监视器对应的对象
private static Object monitor = new Object();
//生产者线程
static class ProducerThread implements Runnable{
@Override
public void run() {
try {
produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void produce() throws InterruptedException {
while(true){
synchronized(monitor){
//循环检测库存是否大于0,大于0表示还有商品可以消费,线程等待消费者消费商品
while(storeMount > 0){
monitor.wait();
}
//被唤醒后获取到对象的监视器之后执行的代码
System.out.println("Thread "+Thread.currentThread().getName()+" begin produce goods");
//生产商品
storeMount = 1;
//唤醒消费者
monitor.notify();
Thread.sleep(1000);
}
}
}
}
//消费者线程
static class ConsumerThread implements Runnable{
@Override
public void run() {
try {
consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void consume() throws InterruptedException {
while(true){
synchronized (monitor){
//检测库存是否不为0,如果不为0,那么有商品可供消费,否则等待生产者生产商品
while(storeMount == 0){
monitor.wait();
}
//消费商品
storeMount = 0;
//唤醒生产者线程
monitor.notify();
System.out.println("Thread "+Thread.currentThread().getName()+" begin consume goods");
Thread.sleep(1000);
}
}
}
}
public static void main(String[] args){
Thread producerThread = new Thread(new ProducerThread());
producerThread.setName("producerThread");
Thread consumerThread = new Thread(new ConsumerThread());
consumerThread.setName("consumerThread");
producerThread.start();
consumerThread.start();
}
}
上述代码示例演示了一个生产者生产商品和一个消费者消费商品的场景,对于一个生产者多个消费者、多个生产者一个消费者、多个生产者多个消费者等场景,只需要将唤醒的方法换为notifyAll()即可,否则,会出现饥饿现象!
以上就是本文叙述的所有内容,本文首先对于给出Java中线程调度形式,引出多线程编程中需要解决的线程安全问题,并分析线程安全问题,给出解决线程安全问题的常用手段(加锁同步),最后,结合Java内置的等待通知机制,进行了样例代码的展示以及分析,给出了经典的等待通知机制的编程范式,最后,基于等待通知机制给出了生产者消费者模式的实现样例,希望本文能给想要学习多线程编程的朋友一点帮助,如有不正确的地方,还望指出,十分感谢!