Java处理线程安全问题

  • Java处理线程安全问题
  • 方法1:同步代码块
  • 同步代码块演示1
  • 同步代码块演示2
  • 同步监视器总结
  • 方法2:同步方法
  • 代码演示
  • 总结
  • 方法3:Lock锁
  • Lock锁引入
  • 代码演示
  • Lock和synchronized的区别
  • 优先使用顺序
  • 线程同步的优缺点
  • 对比:
  • 可能造成死锁
  • 代码演示


Java处理线程安全问题

方法1:同步代码块

同步代码块演示1

public class BuyTicketThread implements Runnable {
    int ticketNum = 10;
    @Override
    public void run() {
 
        for (int i = 1; i <= 100 ; i++) {
            synchronized (this){//把具有安全隐患的代码锁住即可,如果锁多了就会效率低 --》this就是这个锁
                if(ticketNum > 0){
                    System.out.println("我在"+Thread.currentThread().getName()+"买到了北京到哈尔滨的第" + ticketNum-- + "张车票");
                }
            }
        }
    }
}

同步代码块演示2

public class BuyTicketThread extends Thread {
    public BuyTicketThread(String name){
        super(name);
    }
    //一共10张票:
    static int ticketNum = 10;//多个对象共享10张票
    //每个窗口都是一个线程对象:每个对象执行的代码放入run方法中
    @Override
    public void run() {
        //每个窗口后面有100个人在抢票:
        for (int i = 1; i <= 100 ; i++) {
            synchronized (BuyTicketThread.class){//锁必须多个线程用的是同一把锁!!!
                if(ticketNum > 0){//对票数进行判断,票数大于零我们才抢票
                    System.out.println("我在"+this.getName()+"买到了从北京到哈尔滨的第" + ticketNum-- + "张车票");
                }
            }
        }
    }
}

同步监视器总结

  • 总结1:认识同步监视器(锁子) ----- synchronized(同步监视器){ }
  • 1)必须是引用数据类型,不能是基本数据类型
  • 2)也可以创建一个专门的同步监视器,没有任何业务含义
  • 3)一般使用共享资源做同步监视器即可
  • 4)在同步代码块中不能改变同步监视器对象的引用

bitset java 线程安全 java解决线程安全_多线程

  • 5)尽量不要String和包装类Integer做同步监视器
  • 6)建议使用final修饰同步监视器
  • 总结2:同步代码块的执行过程
  • 1)第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码
  • 2)第一个线程执行过程中,发生了线程切换(阻塞 就绪),第一个线程失去了cpu,但是没有开锁open
  • 3)第二个线程获取了cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
  • 4)第一个线程再次获取CPU,接着执行后续的代码;同步代码块执行完毕,释放锁open
  • 5)第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)
  • 强调:同步代码块中能发生CPU的切换吗?能!!! 但是后续的被执行的线程也无法执行同步代码块(因为锁仍旧close)
  • 总结3:其他
  • 1)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块
  • 2)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块, 但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块

方法2:同步方法

代码演示

public class BuyTicketThread implements Runnable {
    int ticketNum = 10;
    @Override
    public void run() {

        for (int i = 1; i <= 100 ; i++) {
            buyTicket();
        }

    }
    public synchronized void buyTicket(){//锁住的是this
        if(ticketNum > 0){
            System.out.println("我在"+Thread.currentThread().getName()+"买到了北京到哈尔滨的第" + ticketNum-- + "张车票");
        }
    }
}
public class BuyTicketThread extends Thread {
    public BuyTicketThread(String name){
        super(name);
    }
    //一共10张票:
    static int ticketNum = 10;//多个对象共享10张票
    //每个窗口都是一个线程对象:每个对象执行的代码放入run方法中
    @Override
    public void run() {
        //每个窗口后面有100个人在抢票:
        for (int i = 1; i <= 100 ; i++) {
            buyTicket();
        }
    }
    public static synchronized void buyTicket(){//锁住的  同步监视器: BuyTicketThread.class
        if(ticketNum > 0){//对票数进行判断,票数大于零我们才抢票
            System.out.println("我在"+Thread.currentThread().getName()+"买到了从北京到哈尔滨的第" + ticketNum-- + "张车票");
        }
    }
}

总结

  • 总结1:
  • 多线程在争抢资源,就要实现线程的同步(就要进行加锁,并且这个锁必须是共享的,必须是唯一的。
  • 锁一般都是引用数据类型的。
  • 目的:解决了线程安全问题。
  • 总结2:关于同步方法
  1. 不要将run()定义为同步方法
  2. 非静态同步方法的同步监视器是this
    静态同步方法的同步监视器是 类名.class 字节码信息对象
  3. 同步代码块的效率要高于同步方法
  • 原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部
  1. 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法;同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块

方法3:Lock锁

Lock锁引入

  • JDK1.5后新增新一代的线程同步方式:Lock锁
  • 与采用synchronized相比,lock可提供多种锁方案,更灵活
  • synchronized是Java中的关键字,这个关键字的识别是靠JVM来识别完成的呀。是虚拟机级别的。
  • 但是Lock锁是API级别的,提供了相应的接口和对应的实现类,这个方式更灵活,表现出来的性能优于之前的方式。

代码演示

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BuyTicketThread implements Runnable {
    int ticketNum = 10;
    //拿来一把锁:
    Lock lock = new ReentrantLock();//多态  接口=实现类  可以使用不同的实现类
    @Override
    public void run() {
        //此处有1000行代码
        for (int i = 1; i <= 100 ; i++) {
            //打开锁:
            lock.lock();
            try{
                if(ticketNum > 0){
                    System.out.println("我在"+Thread.currentThread().getName()+"买到了北京到哈尔滨的第" + ticketNum-- + "张车票");
                }
            }catch (Exception ex){
                ex.printStackTrace();
            }finally {
                //关闭锁:--->即使有异常,这个锁也可以得到释放
                lock.unlock();
            }
        }
    }
}

Lock和synchronized的区别

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

优先使用顺序

  • Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)

线程同步的优缺点

对比:

  • 线程安全,效率低
  • 线程不安全,效率高

可能造成死锁

  • 死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

代码演示

public class TestDeadLock implements Runnable {
    public int flag = 1;
    static Object o1 = new Object(),o2 = new Object();
	public void run(){
    System.out.println("flag=" + flag);
    // 当flag==1锁住o1
    if (flag == 1) {
        synchronized (o1) {
            try {
                Thread.sleep(500);
            } catch (Exception e) {
                e.printStackTrace();
            }
            // 只要锁住o2就完成
            synchronized (o2) {
                System.out.println("2");
            }
        }
    }
    // 如果flag==0锁住o2
    if (flag == 0) {
        synchronized (o2) {
            try {
                Thread.sleep(500);
            } catch (Exception e) {
                e.printStackTrace();
            }
            // 只要锁住o1就完成
            synchronized (o1) {
                System.out.println("3");
            }
        }
    }
}

public static void main(String[] args) {
    // 实例2个线程类
    TestDeadLock td1 = new TestDeadLock();
    TestDeadLock td2 = new TestDeadLock();
    td1.flag = 1;
    td2.flag = 0;
    // 开启2个线程
    Thread t1 = new Thread(td1);
    Thread t2 = new Thread(td2);
    t1.start();
    t2.start();
}
  • 解决方法: 减少同步资源的定义,避免嵌套同步