Java 多线程 解决线程安全问题的三种线程同步机制

先引入线程的生命周期

在这里插入图片描述在这里插入图片描述

线程同步问题

在这里插入图片描述再引入问题:模拟火车站售票程序,开启三个窗口售票。

package com.atguigu.java;
/**
 * 使用Runnable接口的方式来实现
 */
class window1 implements Runnable{
    private  int ticket=100;     //注意此处没有加static,但三个线程共用同一个ticket
    @Override
    public void run() {
        while(true){
            if (ticket>0){
                try {
                    Thread.sleep(100);   //此处会发生阻塞,使用sleep()来增加程序出错几率
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":票成功售出,票号为:"+ticket);
                ticket--;
            }else{
                System.out.println("票已经售空");
                break;
            }
        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        window1 w =new window1();    //只造一个实现类对象
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}


运行发现:
问题一:(出现重票,线程不安全)
在这里插入图片描述
问题二:(出现错票,票号不合法问题。线程不安全)
在这里插入图片描述出现“票号-1”的原因:
在这里插入图片描述
问题出现的根源:
当某个线程操作车票(车票为共享数据)的过程中,尚未操作完成时,其他线程参与进来,也操作车票。

实际情境:比如a在厕所一间隔间上厕所,然后a还没有上完厕所时,b就冲了进来,b也要上厕所。那这就很不安全了…

请添加图片描述
如果我们把门锁上,谁进去谁就拿着这把锁(拿到锁的可以操作),没进去就拿不到这把锁。

所以如何解决:
当一个线程A在操作共享数据时,其他线程不能参与进来,直到线程A操作结束时,其他线程才可以操作。这种情况下,即使线程A出现了阻塞,也不能被改变。

在Java中,我们通过同步机制,来解决线程的安全问题

方式一:同步代码块解决线程安全问题

格式:

synchronized(同步监视器){
//需要被同步的代码
}

如下代码即可解决上述售票程序的两个问题:

package com.atguigu.java;
class window1 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--;
                } else {
                    System.out.println("票已经售空");
                    break;
                }
            }
        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        window1 w =new window1();    //只造一个实现类对象
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

实际上不需要new一个Object对象,在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。

package com.atguigu.java;
class window1 implements Runnable{
    private  int ticket=100;
    @Override
    public void run() {
        while(true) {
            synchronized (this) {      //传入this可行。此处this表示唯一的window1的对象w
                if (ticket > 0) {      //把操作共享数据的代码包起来
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":票成功售出,票号为:" + ticket);
                    ticket--;
                } else {
                    System.out.println("票已经售空");
                    break;
                }
            }
        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        window1 w =new window1();    //只造一个实现类对象
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

说明:
1,操作共享数据的代码,即为需要被同步的代码。
2,共享数据即多个线程共同操作的变量,如ticket
3,同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。但要求多个线程必须要共用同一把锁

优点: 解决线程的安全问题。
缺点: 操作同步代码时,只能有一个线程参与,其他线程等待,相当于一个单线程的过程,效率低。

练习:
使用同步代码块方式处理继承Thread类的线程安全问题

package com.atguigu.java;
/**
 * 例子:创建三个窗口买票,总票数为100张
 * 使用同步代码块解决继承Thread类的方式的线程安全问题
 */
public class WindowTest {
    public static void main(String[] args) {
        Window t1 =new Window();
        Window t2 =new Window();
        Window t3 =new Window();
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}

class Window extends Thread{
    static Object obj=new Object();   //注意此处必须加static来保证三个window对象共用一个obj
    private static int ticket=100;    //static。三个窗口共享同一个ticket
    @Override
    public void run() {
        while(true){
            //正确的
            synchronized (obj){
            //而此处用synchronized(this){ 是错误的。因为此处的this代表着t1,t2,t3三个对象,对象(锁)不唯一,线程不安全
                if (ticket>0){
                    try {
                        Thread.sleep(100);      //增大线程不安全现象出现的几率(使程序阻塞一段时间)
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName()+":票成功售出,票号为:"+ticket);
                    ticket--;
                }else{
                    System.out.println("票已经售空");
                    break;
                }
            }
        }
    }
}

方式二:同步方法解决线程安全问题

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。

①使用同步方法解决实现Runnable接口的线程安全问题

package com.atguigu.java;
/*
 * 使用同步方法解决实现Runnable接口的线程安全问题
 */
class window3 implements Runnable{
    private  int ticket=100;     //注意此处没有加static,但三个线程共用同一个ticket,因为共用同一个window3对象
    @Override
    public void run() {
        while(true){
            show();  //调用show方法
        }
    }
    //定义一个show方法
    private synchronized void show(){   //声明为同步方法。有同步监视器,此处默认为this
        if (ticket>0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":票成功售出,票号为:"+ticket);
            ticket--;
        }
    }
}
public class WindowTest3 {
    public static void main(String[] args) {
        window3 w =new window3();    //只造一个实现类对象
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

②使用同步方法解决继承Thread类方式中的线程安全问题

package com.atguigu.java;
/**
 * 例子:创建三个窗口买票,总票数为100张
 * 使用同步方法解决继承Thread类方式中的线程安全问题
 */
public class WindowTest4 {
    public static void main(String[] args) {
        Window t1 =new Window();
        Window t2 =new Window();
        Window t3 =new Window();
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}

class Window extends Thread{
    private static int ticket=100;    //static。三个窗口共享同一个ticket
    @Override
    public void run() {
        while(true){
            show();      //非静态方法也可以调用静态结构
        }
    }
    private static synchronized void show(){//若想要用同步方法解决线程不安全需要将方法设为静态,此时的同步监视器(静态同步方法为当前类:window.class)才唯一
    //private synchronized void show(){  //此方法不正确。同步监视器this指的是:t1,t2,t3。
        if (ticket>0){
            try {
                sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //静态方法只能调用静态结构。通过对象调用线程名保证不报错(Thread.currentThread().getName())
            System.out.println(Thread.currentThread().getName()+":票成功售出,票号为:"+ticket);
            ticket--;
        }
    }
}

关于同步方法的总结:
①同步方法仍然涉及到同步监视器,只是不需要我们显式地声明;
② 对于非静态的同步方法,同步监视器是this;
对于静态的同步方法,同步监视器是当前类本身


解决懒汉式单例模式的线程安全问题

使用同步方法改造懒汉式单例模式

package com.atguigu.java1;
//使用同步方法将单例模式中的懒汉式实现修改为线程安全
public class BankTest {

}
class Bank{
    private Bank(){

    }
    private static Bank instance=null;  //instance当作共享数据
    public static synchronized Bank getInstance(){   //对于静态的同步方法,同步监视器是当前类本身。此处为Bank.class
        if (instance==null){     //如果不同步,此处可能会出现阻塞,有线程安全问题
            instance=new Bank();
        }
        return instance;
    }
}

使用同步代码块改造懒汉式单例模式

情形一代码:(效率稍差)

package com.atguigu.java1;
/**
 * 使用同步代码块将单例模式中的懒汉式实现修改为线程安全的
 */
public class BankTest {

}
class Bank{
    private Bank(){

    }
    private static Bank instance=null;  //instance当作共享数据
    public static Bank getInstance(){
        synchronized (Bank.class) {      
            if (instance==null){     //如果不同步,此处可能会出现阻塞,有线程安全问题
                instance=new Bank();
            }
            return instance;
        }
    }
}

上述情形一代码效率稍差。当多个线程走到synchronized结构时,线程一拿到锁之后进入此结构,造一个对象,然后return;后面的线程进来后判断发现,对象不为空,就直接把线程一造好的对象进行return。

就像鸡排店剩下今天最后一份大脸鸡排,后面队伍还很长,第一个人进去买完带着鸡排回去了,后面的人都继续排队询问还有没有鸡排。

这显然效率不高,所以老板可以直接贴出公告说明已经卖完,后面的不用再等了。

情形二代码:(效率稍高,面试时写这个)

package com.atguigu.java1;
/**
 * 使用同步机制将单例模式中的懒汉式实现修改为线程安全的
 */
public class BankTest {

}
class Bank{
    private Bank(){

    }
    private static Bank instance=null;  
    public static Bank getInstance(){
        //效率稍高
       if (instance==null){
           synchronized (Bank.class) {       //里面认为是在操作共享数据
               if (instance==null){
                   instance=new Bank();
               }
           }
       }
       return instance;
    }
}

死锁问题

我们使用同步时,要避免出现死锁。
在这里插入图片描述比如做了一桌饭,两个人吃,只有一双筷子。如果一个人先拿到这双筷子就可以先吃,吃完再让另一个人吃;但是如果一人拿了一只筷子,都在等对方放弃自己的筷子,僵持不下,程序就进入了死锁状态。

package com.atguigu.java1;
/**
 * 演示线程的死锁问题
 */
public class ThreadTest {
    public static void main(String[] args) {
        StringBuffer s1=new StringBuffer();
        StringBuffer s2=new StringBuffer();
        new Thread(){    //匿名创建线程,继承Thread类方式
            @Override
            public void run() {
                synchronized (s1){
                    s1.append("a");       //给字符串添加一个a
                    s2.append("1");
                    try {
                        Thread.sleep(100);    //增加死锁现象出现的概率,注意sleep()不会释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s2){    //嵌套
                        s1.append("b");
                        s2.append("1");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
        //实现Runnable接口方式
        new Thread(new Runnable() {      //传入一个匿名的Runnable接口实现类的对象
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("c");
                    s2.append("3");

                    try {
                        Thread.sleep(100);    //增加死锁现象出现的概率
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1){    //嵌套
                        s1.append("d");
                        s2.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

第一个线程握住锁s1,调用append()添加字符,然后执行到sleep()时会进入阻塞状态。此时第二个线程就可能会执行,握住锁s2,调用append()添加字符,也然后执行到sleep()。当二者sleep结束时,第一个线程拿着s1等着拿s2,第二个线程拿着s2等着拿s1。二者就会“僵持不下”。
在这里插入图片描述

程序不抛异常,也不终止----->死锁


死锁演示:

package com.atguigu.java1;
//死锁演示
class A {
	public synchronized void foo(B b) {  //foo方法为同步方法。锁(this)为A的对象a
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了A实例的foo方法"); // ①
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}

		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用B实例的last方法"); // ③
		b.last();
	}

	public synchronized void last() {  //同步监视器(this):A类的对象a
		System.out.println("进入了A类的last方法内部");  //同步监视器:A类的对象a
	}
}

class B {
	public synchronized void bar(A a) {  //同步监视器(this)为b
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了B实例的bar方法"); // ②
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用A实例的last方法"); // ④
		a.last();    //所以主线程要先握a后握b,此方法才能结束,接下来才能释放b和a
	}

	public synchronized void last() {  //同步监视器(this)为b对象
		System.out.println("进入了B类的last方法内部");
	}
}

public class DeadLock implements Runnable {
	A a = new A();
	B b = new B();

	public void init() {
		Thread.currentThread().setName("主线程");
		// 调用a对象的foo方法
		a.foo(b);
		System.out.println("进入了主线程之后");
	}

	public void run() {
		Thread.currentThread().setName("副线程");
		// 调用b对象的bar方法
		b.bar(a);
		System.out.println("进入了副线程之后");
	}

	public static void main(String[] args) {
		DeadLock dl = new DeadLock();   //造当前类的对象
		new Thread(dl).start();     //相当于创建一个分线程,启动
		dl.init();  //主线程调用init()方法
	}
}


①主线程要先握a后握b,foo方法才能结束,接下来才能释放b和a。
②分线程要先握b后握a,bar方法才能结束,接下来才能释放b和a。

会出现死锁问题。

方式三:Lock锁解决线程安全问题

在这里插入图片描述格式:
在这里插入图片描述

仍以“三个窗口售票100张”为例感受Lock锁用法:

package com.atguigu.java1;
import java.util.concurrent.locks.ReentrantLock;
/**
 * Lock锁解决线程安全问题----jdk5.0新增。Lock本身是一个接口,具体使用的是它的实现类ReentrantLock
 */
class Window0 implements Runnable{  //注意此处为实现Runnable接口方式下使用Lock锁。继承Thread方式需要使用static修饰实现类对象
    private int ticket=100;
    //①创建一个ReentrantLock对象(实例化ReentrantLock)
    private ReentrantLock lock=new ReentrantLock();

    @Override
    public void run() {
        while (true){
            try {             //要操作的代码放到try中
                //②调用锁定方法lock()。表示在此过程中它是一个单线程的,
                lock.lock();
                if (ticket>0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":成功售出,票号为:"+ticket);
                    ticket--;
                }else{
                    System.out.println("票卖完了");
                    break;
                }
            }finally{      //一定会执行
                lock.unlock(); //③调用解锁方法unlock()。
            }
        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Window0 w=new Window0();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}


常问的一个面试题:
synchronized 与 Lock 的异同?
同:二者都可以解决线程安全问题。
异:synchronized 机制执行完相应的同步代码之后自动释放同步监视器。Lock 需要手动去启动同步(lock()方法),手动结束同步(unlock()方法),更灵活。

练习:

在这里插入图片描述

分析:

  • ①两个储户涉及两个线程;
  • ②存在共享数据,账户(余额)。可能存在线程安全问题(都操作了共享数据);
    我们有三种方式解决线程安全问题。
    我们选用同步方法方式:
package com.atguigu.java1;
/**
 * 银行有一个账户。
 * 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打
 * 印账户余额。
 */
class Account{
    private double balance;
    //构造器
    public Account(double balance) {
        this.balance = balance;
    }
    //存钱方法设为同步方法。一个人存完之后,另一个人才能存
    public synchronized void deposit(double amt){       //此处的锁(this)为acct,唯一。
        if (amt>0){
            balance+=amt;
            try {
                Thread.sleep(1000);         //增大线程不安全问题出现概率
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"存钱成功。余额为:"+balance);  //此处有线程安全问题
        }
    }
}

class Customer extends Thread{
    private Account acct;

    public Customer(Account acct) {        //alt+insert快捷键选择构造器
        this.acct = acct;
    }
    //重写run()
    @Override
    public void run() {    //要执行的操作是存钱
        for (int i = 0; i < 3; i++) {
            acct.deposit(1000);
        }
    }
}

public class AccountTest {
    public static void main(String[] args) {   //这里面需要new两个储户
        Account acct=new Account(0);
        Customer c1 = new Customer(acct);
        Customer c2 = new Customer(acct);   //此时两个对象共用同一个账户
        //给线程起名字
        c1.setName("甲");
        c2.setName("乙");
        c1.start();
        c2.start();

    }
}

在这里插入图片描述

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

过期动态

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值