Java 多线程的同步代码块详解

目录
  • synchronized
    • 同步代码块
    • 同步方法(this锁)
    • 静态同步方法
    • 死锁问题
  • lock
  • 总结

火车站抢票问题

由于现实中买票也不会是零延迟的,为了真实性加入了延迟机制,也就是线程休眠语句

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();
    }
}

结果正确

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • 总结java多线程之互斥与同步解决方案

    一.线程互斥与同步 互斥:指的是多个线程不能同时访问共享变量 同步:指的是多个线程按指定的顺序执行操作 在同时有多个线程运行过程中,如何达到互斥和同步呢? 加锁即可 在此使用黑马笔记中room例子来说明锁.(ps: 以前就了解锁,但总会记乱,发现使用形象化记忆后就很清楚) 解决互斥 锁就相当于上图的房子,里面放着会被并发访问的共享变量 此时绿色区域(owner)无线程,此时多个线程想并发访问房子里的共享变量,那么只允许其中一个线程进入房子访问,并把房门锁上. 剩下的没有拿到锁的线程只能在entr

  • Java多线程的同步优化的6种方案

    概述 处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存. 加入高速缓存带来了一个新的问题:缓存一致性.如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题. 在Java内存模型中,分为主内存和线程工作内存,线程使用共享数据时,先从主内存中拷贝数据到工作内存,使用完成之后再写入主内存中. 在Java中,有多线程并发时,我们可以使用多线程同步的方式来解决内存一致性的问题.通常我们可以在程序中添加同步锁来保障数据的安

  • Java多线程之同步工具类CyclicBarrier

    目录 1 CyclicBarrier方法说明 2 CyclicBarrier实例 3 CyclicBarrier源码解析 CyclicBarrier构造函数 await方法 nextGeneration的源码 breakBarrier源码 isBroken方法 reset方法 getNumberWaiting方法 前言: CyclicBarrier是一个同步工具类,它允许一组线程互相等待,直到达到某个公共屏障点.与CountDownLatch不同的是该barrier在释放线程等待后可以重用,所以

  • Java多线程之同步工具类CountDownLatch

    前言: CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程执行完后再执行.例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有框架服务之后执行. 1 CountDownLatch主要方法 void await():如果当前count大于0,当前线程将会wait,直到count等于0或者中断. PS:当count等于0的时候,再去调用await() , 线程将不会阻塞,而是立即运行.后面可以通过源码分析得到. boolean await(long t

  • Java多线程之线程同步

    volatile 先看个例子 class Test { // 定义一个全局变量 private boolean isRun = true; // 从主线程调用发起 public void process() { test(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } stop(); } // 启动一个子线程循环读取isRun private void test() {

  • Java多线程之同步工具类Exchanger

    目录 1 Exchanger 介绍 2 Exchanger 实例 exchange等待超时 3 实现原理 1 Exchanger 介绍 前面分别介绍了CyclicBarrier.CountDownLatch.Semaphore,现在介绍并发工具类中的最后一个Exchange. Exchanger 是一个用于线程间协作的工具类,Exchanger用于进行线程间的数据交换,它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据.这两个线程通过exchange 方法交换数据,如果第一个线程先执行e

  • Java 多线程的同步代码块详解

    目录 synchronized 同步代码块 同步方法(this锁) 静态同步方法 死锁问题 lock 总结 火车站抢票问题 由于现实中买票也不会是零延迟的,为了真实性加入了延迟机制,也就是线程休眠语句 package test.MyThread.ticketDemo; public class RunnableThread implements Runnable{ private int ticket = 100; @Override public void run(){ while(true)

  • Java多线程之synchronized同步代码块详解

    目录 1.同步方法和同步块,哪种更好? 2.synchronized同步代码块 3.如果同步块内的线程抛出异常会发生什么? 总结 面试题: 1同步方法和同步块,哪种更好? 2.如果同步块内的线程抛出异常会发生什么? 1. 同步方法和同步块,哪种更好? 同步块更好,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率.请知道一条原则:同步的范围越小越好. 对于小的临界区,我们直接在方法声明中设置synchronized同步关键字,可以避免竞态条件的问题.但是对于较大的临界区代码段

  • Java编程中的4种代码块详解

    在Java编程中,代码块就是指用"{}"括起来的代码.下面看一下这四种代码块. 1.普通代码块 就是指类中方法的方法体. public void xxx(){ //code } 2.构造块 下面用"{}"括起来的代码片段,构造块在创建对象时会被调用,每次创建对象时都会被调用,并且优先于类构造函数(包括有参和无参的)执行. 构造块中定义的变量是局部变量. public class Client { {//构造代码块 System.out.println("执

  • Java杂谈之类和对象 封装 构造方法以及代码块详解

    目录 1. 类和对象的初步认知 2. 类的实例化 3. 类的成员 字段(属性/成员变量) 方法 static 关键字 修饰字段 修饰方法 修饰代码块(暂不讲) 修饰类(暂不讲) 4. 封装 5. 构造方法 6. this 用法 关于引用的几个注意事项: 7. 代码块 Java当中的类和对象 1. 类和对象的初步认知 java 是一门面向对象的语言,所谓面向对象有别于面向过程,面向对象是只需对象之间的交互即可完成任务,但是面向过程的话,需要我们将每一个步骤都详细地做出来.比如,以洗衣服为例,如果是

  • Java多线程中ReentrantLock与Condition详解

    一.ReentrantLock类 1.1什么是reentrantlock java.util.concurrent.lock中的Lock框架是锁定的一个抽象,它允许把锁定的实现作为Java类,而不是作为语言的特性来实现.这就为Lock的多种实现留下了空间,各种实现可能有不同的调度算法.性能特性或者锁定语义.ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,但是添加了类似锁投票.定时锁等候和可中断锁等候的一些特性.此外,它还提供了在激烈争用情况下更

  • java多线程关键字final和static详解

    这篇文章主要介绍了java多线程关键字final和static详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 final关键字 1.final关键字在单线程中的特点: 1)final修饰的静态成员:必须在进行显示初始化或静态代码块赋值,并且仅能赋值一次. 2)final修饰的类成员变量,可以在三个地方进行赋值:显示初始化.构造代码块和构造方法,并且仅能赋值一次. 3)final修饰的局部变量,必须在使用之前进行显示初始化(并不一定要在定义是

  • Java多线程之搞定最后一公里详解

    目录 绪论 一:线程安全问题 1.1 提出问题 1.2 不安全的原因 1.2.1 原子性 1.2.2 代码"优化" 二:如何解决线程不安全的问题 2.1 通过synchronized关键字 2.2 volatile 三:wait和notify关键字 3.1 wait方法 3.2 notify方法 3.3 wait和sleep对比(面试常考) 四:多线程案例 4.1 饿汉模式单线程 4.2 懒汉模式单线程 4.3 懒汉模式多线程低性能版 4.4懒汉模式-多线程版-二次判断-性能高 总结

  • java 多线程与并发之volatile详解分析

    目录 CPU.内存.缓存的关系 CPU缓存 什么是CPU缓存 为什么要有多级CPU Cache Java内存模型(Java Memory Model,JMM) JMM导致的并发安全问题 可见性 原子性 有序性 volatile volatile特性 volatile 的实现原理 总结 CPU.内存.缓存的关系 要理解JMM,要先从计算机底层开始,下面是一份大佬的研究报告 计算机在做一些我们平时的基本操作时,需要的响应时间是不一样的!如果我们计算一次a+b所需要的的时间: CPU读取内存获得a,1

  • Java多线程读写锁ReentrantReadWriteLock类详解

    目录 ReentrantReadWriteLock 读读共享 写写互斥 读写互斥 源码分析 写锁的获取与释放 读锁的获取与释放 参考文献 真实的多线程业务开发中,最常用到的逻辑就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务),这样做虽然保证了实例变量的线程安全性,但效率却是非常低下的.所以在JDK中提供了一种读写锁ReentrantReadWriteLock类,使用它可以加快运行效率. 读写锁表示两个锁,一个是读操作相关的锁

  • Java多线程模拟银行系统存钱问题详解

    目录 一.题目描述 二.解题思路 三.代码详解 多学一个知识点 一.题目描述 题目:模拟一个简单的银行系统,使用两个不同的线程向同一个账户存钱. 实现:使用特殊域变量volatile实现同步. 二.解题思路 创建一个类:SynchronizedBankFrame,继承JFrame类 写一个内部类Bank 定义一个account变量,来表示账户. deposit():一个存钱的方法 getAccount():显示账户余额的方法. 写一个内部类Transfer,实现Runnable接口 在run方法

随机推荐