文章目录:
1.为什么要实现多线程同步?
2.线程安全
2.1 什么是线程安全?(卖电影票实例)
3.多线程同步的三种实现方式
3.1 同步代码块
3.2 同步方法
3.3 同步锁
1.为什么要实现多线程同步?
多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。
2.线程安全
线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决线程安全问题,必须得保证处理共享资源的代码在任意时刻只能有一个线程访问。为此,Java中提供了线程同步机制。
假如Java程序中有多个线程在同时运行,而这些线程可能会同时运行一部分的代码。如果说该Java程序每次运行的结果和单线程的运行结果是一样的,并且其他的变量值也都是和预期的结果是一样的,那么就可以说线程是安全的。
2.1 什么是线程安全?(卖电影票实例)
①情况1:该电影院开设一个售票窗口,一个窗口卖一百张票,没有问题。就如同单线程程序不会出现安全问题一样。
②情况2:该电影院开设n(n>1)个售票窗口,每个售票窗口售出指定号码的票,也不会出现问题。就如同多线程程序,没有访问共享数据,不会产生问题。
③情况3:该电影院开设n(n>1)个售票窗口,每个售票窗口出售的票都是没有规定的(如:所有的窗口都可以出售1号票),这就会出现问题了,假如三个窗口同时在卖同一张票,或有的票已经售出,还有窗口还在出售。就如同多线程程序,访问了共享数据,会产生线程安全问题。
下面就是情况3对应的程序代码:???
class MovieTicket01 implements Runnable {private static int ticketNumber=10;//电影票数量@Overridepublic void run() {while(ticketNumber>0) {try {//提高程序安全的概率,让线程先睡眠10msThread.sleep(10);}catch(InterruptedException e) {e.printStackTrace();}//电影票出售System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket01.ticketNumber + "号电影票");ticketNumber--;//出售一张就自减1}}
}
public class Thread04 {public static void main(String[] args) {//创建Runnable接口子类的实现对象MovieTicket01 movieTicket=new MovieTicket01();//创建Thread线程类对象Thread window1=new Thread(movieTicket);Thread window2=new Thread(movieTicket);Thread window3=new Thread(movieTicket);//为这三个线程命名window1.setName("window1");window2.setName("window2");window3.setName("window3");//调用start()方法启动线程window1.start();window2.start();window3.start();}
}
大家一看这个输出结果肯定就会发现问题,窗口1卖出了10号电影票,然后窗口3和窗口2竟然还在卖10号电影票,这显然是不符合逻辑的吧!!!三个窗口(线程)同时出售不指定号数的票(访问共享数据),出现了卖票重复的情况。
出现这种情况的原因就是因为JVM默认的是抢占调度方式,三个线程谁先抢到CPU谁执行,所以自然就乱套了。要解决这个问题,我们就需要通过多线程同步、安全来实现。
3.多线程同步的三种实现方式
3.1 同步代码块
注意:lock锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是相同的。锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁。
原理:①当线程执行同步代码块时,首先会检查lock锁对象的标志位;
②默认情况下标志位为1,此时线程会执行Synchronized同步代码块,同时将锁对象的标志位置为0;
③当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后;
④锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码,这样循环往复,直到共享资源被处理完为止。
class MovieTicket02 implements Runnable {private static int ticketNumber=10;//电影票数量/*不能放在run()方法中!!!否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁。*/Object object=new Object();//创建锁对象@Overridepublic void run() {//同步代码块synchronized(object) {//设置此线程要执行的任务while(ticketNumber>0) {try {//提高程序安全的概率,让线程先睡眠10msThread.sleep(10);}catch(InterruptedException e) {e.printStackTrace();}//电影票出售System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket02.ticketNumber + "号电影票");ticketNumber--;//出售一张就自减1}}}
}
public class Thread05 {public static void main(String[] args) {//创建Runnable接口子类的实现对象MovieTicket02 movieTicket=new MovieTicket02();//创建Thread线程类对象Thread window1=new Thread(movieTicket);Thread window2=new Thread(movieTicket);Thread window3=new Thread(movieTicket);//为这三个线程命名window1.setName("window1");window2.setName("window2");window3.setName("window3");//调用start()方法启动线程window1.start();window2.start();window3.start();}
}
这个时候,控制台不再出售不存在的电影号数以及重复的电影号数了。
通过代码块中的锁对象,可以使用任意的对象。但是必须保证多个线程使用的锁对象是同一。锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行。
总结:同步中的线程,没有执行完毕,不会释放锁,同步外的线程,没有锁,进不去同步。
3.2 同步方法
说明:在方法前面也可以使用 synchronized 关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能。被 synchronized 修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行。
注意:①同步方法也有锁,它的锁就是当前调用该方法的对象,就是this指向的对象。
②Java中静态方法的锁是该方法所在类的class对象,该对象可以直接类名.class的方式获取。
③同步代码块和同步方法解决多线程问题有好处也有弊端。同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行,但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。
class MovieTicket03 implements Runnable {private static int ticketNumber=10;//电影票数量/*不能放在run()方法中!!!否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁。*/Object object=new Object();//创建锁对象@Overridepublic void run() {ticket();//设置此线程要执行的任务}//同步方法public synchronized void ticket() {while(ticketNumber>0) {try {//提高程序安全的概率,让线程先睡眠10msThread.sleep(10);}catch(InterruptedException e) {e.printStackTrace();}//电影票出售System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket03.ticketNumber + "号电影票");ticketNumber--;//出售一张就自减1}}
}
public class Thread06 {public static void main(String[] args) {//创建Runnable接口子类的实现对象MovieTicket03 movieTicket=new MovieTicket03();//创建Thread线程类对象Thread window1=new Thread(movieTicket);Thread window2=new Thread(movieTicket);Thread window3=new Thread(movieTicket);//为这三个线程命名window1.setName("window1");window2.setName("window2");window3.setName("window3");//调用start()方法启动线程window1.start();window2.start();window3.start();}
}
这里的输出结果和同步代码块是一样的!!!
3.3 同步锁
问题:synchronized同步代码块和同步方法使用一种封闭式的锁机制,使用起来非常简单,也能够解决线程同步过程中出现的线程安全问题,但也有一些限制,例如它无法中断一个正在等候获得锁的线程,也无法通过轮询得到锁,如果不想等下去,也就没法得到锁。
解决:从JDK 5开始,Java增加了一个功能更强大的Lock锁。Lock锁与synchronized隐式锁在功能上基本相同,其最大的优势在于Lock锁可以让某个线程在持续获取同步锁失败后返回,不再继续等待,另外Lock锁在使用时也更加灵活。
注意:
ReentrantLock类是Lock锁接口的实现类,也是常用的同步锁,在该同步锁中除了 lock() 方法和 unlock() 方法外,还提供了一些其他同步锁操作的方法,例如 tryLock() 方法可以判断某个线程锁是否可用。
在使用Lock同步锁时,可以根据需要在不同代码位置灵活的上锁和解锁,为了保证所有情况下都能正常解锁以确保其他线程可以执行,通常情况下会在 finally{} 代码块中调用 unlock() 方法来解锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class MovieTicket04 implements Runnable {private static int ticketNumber=10;//电影票数量Lock reentrantLock=new ReentrantLock();//锁对象@Overridepublic void run() {while(ticketNumber>0) {reentrantLock.lock();//加锁try { Thread.sleep(20);//电影票出售System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket04.ticketNumber + "号电影票");ticketNumber--;//出售一张就自减1}catch(InterruptedException e) {e.printStackTrace();}finally {reentrantLock.unlock();//解锁}}}
}
public class Thread07 {public static void main(String[] args) {//创建Runnable接口子类的实现对象MovieTicket04 movieTicket=new MovieTicket04();//创建Thread线程类对象Thread window1=new Thread(movieTicket);Thread window2=new Thread(movieTicket);Thread window3=new Thread(movieTicket);//为这三个线程命名window1.setName("window1");window2.setName("window2");window3.setName("window3");//调用start()方法启动线程window1.start();window2.start();window3.start();}
}
同步锁的方式与前两种方式有所不同。前两种方式,只有线程1能够进入同步机制执行代码,在Lock锁机制中,三个线程都可以进行执行,通过Lock锁机制来解决共享数据问题。