Java 多线程的同步代码块详解
作者:一纸春秋
火车站抢票问题
由于现实中买票也不会是零延迟的,为了真实性加入了延迟机制,也就是线程休眠语句
package test.MyThread.ticketDemo; public class RunnableThread implements Runnable{ private int ticket = 100; @Override public void run(){ while(true){ if(ticket>0){ try { Thread.sleep(100); //语句一 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"正在出售第 "+ticket+" 张票"); //语句二 ticket--; //语句三 } } } }
package test.MyThread.ticketDemo; public class ticketDemo1 { public static void main(String[] args) { RunnableThread r1 = new RunnableThread(); Thread t1 = new Thread(r1,"窗口一"); Thread t2 = new Thread(r1,"窗口二"); Thread t3 = new Thread(r1,"窗口三"); t1.start(); t2.start(); t3.start(); } }
但是结果和我们想象中的不一样,三个窗口卖出了同样的票
这是因为,CPU的操作具有原子性,单独执行一条指令或者说语句,在执行完毕前不会被中断。
三个线程被启动后,都会处于就绪状态,然后开始抢夺CPU执行语句。
1.语句一:Thread.sleep(100);
2.语句二: System.out.println(Thread.currentThread().getName()+“正在出售第 “+ticket+” 张票”);
3.语句三: ticket–;
我将程序中需要执行的三条主要语句列了出来
三条线程中,加入线程一先抢到了CPU,这时就会开始执行语句,也就是至少会完成一条语句一,然后进入休眠。
注:如果语句一不是休眠语句,而是别的语句,那么线程一就可以继续往下执行,因为原子性,正在执行的语句不会被打断,所以只会在一条语句结束,下一条语句未开始时,被抢走CPU或者中断,导致线程退出运行状态,转为就绪或者阻塞状态。所以线程一可以一次性完成多条语句,也有可能刚完成一条语句就被抢走了CPU。
接着,线程二,线程三也抢到了CPU,也开始执行语句一,然后也进入休眠状态。之后线程一二三从休眠中醒来,开始争抢CPU完成语句二,但是三者都在完成语句三之前被抢走了CPU,导致一直没有执行ticket–语句,ticket也就没有减少,因此三条线程一共打印三条输出语句,里面的ticket都是相同。
然后三条线程又开始争抢CPU来完成语句三,一个线程让ticket减一,三个线程减少三张票。完成语句三后,又开始新的循环,三个线程开始争抢CPU完成语句一。
因此,看到的结果会是,三条语句的ticket都相同,然后ticket突然减三,接着又输出三条ticket相同的输出语句。
那么,该如何解决这种情况呢?
这种延迟卖票的问题被称为线程安全问题,要发生线程安全问题需要满足三个条件(任何一共条件不满足都不会造成线程安全问题):
1.是否存在多线程环境
2.是否存在共享数据/共享变量
3.是否有多条语句操作着共享数据/共享变量
火车站延迟卖票问题满足这三个条件,因此造成了线程安全问题,而前两条都不可避免,那么就可以着手于破坏掉第三个条件,让线程安全问题不成立。
思路是将多条语句包装成一个同步代码块,当某个线程执行这个同步代码块的时候,就跟原子性一样,其他的线程不能抢占CPU,只能等这个同步代码块执行完毕。
解决办法:
1.synchronized —— 自动锁
2.lock —— 手动锁
synchronized
synchronized(对象){ //可能会发生线程安全问题的代码 } //这里的对象可以是任意对象,我们可以用 Object obj = new Object()里面的obj放入括号中
使用synchronized的条件:
1.必须有两个或两个以上的线程同一时间只有一个线程能够执行同步代码块多个线程想要同步时,必须共用同一把锁
synchronized(对象)括号里面的对象就是一把锁
使用synchronized的过程:
1.只有抢到锁的线程才可以执行同步代码块,其余的线程即使抢到了CPU执行权,也只能等待,等待锁的释放。
2.代码执行完毕或者程序抛出异常都会释放锁,然后还未执行同步代码块的线程争抢锁,谁抢到谁就能运行同步代码块。
同步代码块
因此,修改后的代码为:
package test.MyThread.ticketDemo; public class RunnableThread implements Runnable{ private int ticket = 100; Object obj = new Object(); @Override public void run(){ while(true){ synchronized (obj) { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第 " + ticket + " 张票"); ticket--; } } } } }
package test.MyThread.ticketDemo; public class ticketDemo1 { public static void main(String[] args) { //这里没有改动,只是在上一个代码中加了一把锁 RunnableThread r1 = new RunnableThread(); Thread t1 = new Thread(r1,"窗口一"); Thread t2 = new Thread(r1,"窗口二"); Thread t3 = new Thread(r1,"窗口三"); t1.start(); t2.start(); t3.start(); } }
可以看出来结果符合我们的预期,是正确的
现在又有了新的问题,那就是如果我在构造线程的RunnableThread类里面加入方法呢?同步代码块里面出现方法时,我们应该怎么“上锁”呢?
同步方法(this锁)
同步方法,在public的后面加上synchronized关键字
package test.MyThread.ticketDemo; public class RunnableThread1 implements Runnable{ private int ticket = 100; Object obj = new Object(); public boolean flag = true; @Override public void run(){ if(flag==true){ while(ticket>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } SellTicket1(); } } } //同步方法,在public的后面加上synchronized关键字 public synchronized void SellTicket1(){ if(ticket>0){ System.out.println(Thread.currentThread().getName()+"正在出售第 "+ticket+" 张票"); ticket--; } } }
package test.MyThread.ticketDemo; public class ticketDemo2 { public static void main(String[] args) throws InterruptedException { RunnableThread1 r = new RunnableThread1(); Thread t1 = new Thread(r,"窗口一"); Thread t2 = new Thread(r,"窗口二"); t1.start(); t2.start(); } }
this锁
先来看看,如果有两条路径,一条路径是使用同步代码块,但是对象是obj,另一条路径是使用同步方法
package test.MyThread.ticketDemo; public class TicketWindow2 implements Runnable{ //定义100张票 private static int tickets = 100; Object obj = new Object(); int i =0; @Override public void run() { while (true){ if(i%2==0){ synchronized (obj){ if(tickets>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票"); } } }else { sellTicket(); } i++; } } public synchronized void sellTicket(){ if(tickets>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票"); } } }
结果出错,说明同步方法的用的对象锁不能是任意的对象,不同的线程应该用相同的锁。同步方法是属于对象,而在这个类里面调用方法的是this对象,也就是this.sellTicket(),因此把this提取出来作为对象锁中的对象。这样多个线程都用的是this锁
package test.MyThread.ticketDemo; public class TicketWindow2 implements Runnable{ //定义100张票 private static int tickets = 100; Object obj = new Object(); int i =0; @Override public void run() { while (true){ if(i%2==0){ synchronized (this){ if(tickets>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票"); } } }else { sellTicket(); } i++; } } public synchronized void sellTicket(){ if(tickets>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票"); } } }
修改完成后再运行代码,发现没有错误
注:
1.一个线程使用同步方法,另一个线程使用同步代码块this锁,可以实现同步
2.一个线程使用同步方法,另一个线程使用同步代码块,但是不是this锁。这种情况不能实现同步。
静态同步方法
同步方法的锁对象是this,
静态同步方法的锁对象是:这个静态同步方法所属的类的字节码文件
下面代码挺长的,但其实就修改了上面同步方法的代码的两处地方
1.public synchronized void sellTicket(){}改为
public synchronized static void sellTicket(){}
2.synchronized (this){}改为synchronized (TicketWindow2.class){}
package test.MyThread.ticketDemo; public class TicketWindow2 implements Runnable{ //定义100张票 private static int tickets = 100; Object obj = new Object(); int i =0; @Override public void run() { while (true){ if(i%2==0){ synchronized (TicketWindow2.class){ if(tickets>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票"); } } }else { sellTicket(); } i++; } } public synchronized static void sellTicket(){ if(tickets>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票"); } } }
main()方法里面创建进程和启动进程的代码,和上面同步方法里面的代码相同
结果也和上面的一样,都不再列出来了
死锁问题
package test.MyThread.ticketDemo; //两个不同的锁对象 public class LockObject { public static final Object lock1 = new Object(); public static final Object lock2 = new Object(); }
package test.MyThread.ticketDemo; public class DieLockThread extends Thread{ public boolean flag; public DieLockThread(boolean flag){ this.flag = flag; } @Override public void run() { if(flag){ synchronized(LockObject.lock1){ System.out.println("lock1"); synchronized(LockObject.lock2){ System.out.println("lock2"); } } }else{ synchronized(LockObject.lock2){ System.out.println("lock2"); synchronized(LockObject.lock1){ System.out.println("lock1"); } } } } }
package test.MyThread.ticketDemo; public class DieLockDemo { public static void main(String[] args) { DieLockThread d1 = new DieLockThread(true); DieLockThread d2 = new DieLockThread(false); d1.start(); d2.start(); } }
程序会卡在这一步,不能进行下一步也不能停止
利用有参构造,构造出来的线程d1应该是先获得锁对象LockObject.lock1然后执行打印语句。接着获取锁对象LockObject.lock2,然后打印lock2。
但是这里因为线程d2是先获取的锁对象LockObject.lock2,并占据这个锁对象,然后想获得锁对象LockObject.lock1,但LockObject.lock1此时被线程d1占据着
两个线程都在等待对方释放锁对象,然后进行下一步,但是两者都不释放,导致程序卡死在这里。这就造成了死锁。
lock
package test.MyThread.ticketDemo; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockThread implements Runnable{ private int ticket = 100; Lock lock = new ReentrantLock(); @Override public void run(){ while(ticket>0){ try{ lock.lock(); if(ticket>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (ticket--) + " 张票"); } }finally { lock.unlock(); } } } }
package test.MyThread.ticketDemo; public class LockDemo { public static void main(String[] args) { LockThread lt = new LockThread(); Thread t1 = new Thread(lt,"窗口一"); Thread t2 = new Thread(lt,"窗口二"); Thread t3 = new Thread(lt,"窗口三"); t1.start(); t2.start(); t3.start(); } }
结果正确
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!