Java 多线程 什么是线程安全问题,同步问题 及其详解!

线程的同步

什么是线程安全问题

在开发中,在多线程数据共享的时候会遇到线程的安全问题,即多个线程同时进行访问时,一个线程对共享的数据还未操作完成,另一个线程也参与进来。

e.g.

public class SellTicket {
    public static void main(String[] args) {
        TicketWindow window = new TicketWindow();

        Thread threadFirst = new Thread(window);
        Thread threadSecond = new Thread(window);
        Thread threadThird = new Thread(window);

        threadFirst.setName("窗口一");
        threadSecond.setName("窗口二");
        threadThird.setName("窗口三");

        threadFirst.start();
        threadSecond.start();
        threadThird.start();
    }
}


class TicketWindow implements Runnable{

    private int ticket = 50;

    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + ": " + ticket);
                ticket--;
            } else {
                break;
            }

        }
    }
}

结果:

因为CPU资源分配是随机的,所以在一个线程执行的过程中很可能插入其他线程,多个线程可能同时访问同一数据,这就使得线程变得不安全。


如何解决

Java中,通过同步机制,来解决问题

方法一:同步代码块

synchronized(同步监视器){
    //需要被同步的代码(操作共享数据的代码)
}

需要同步的代码:操作共享数据的代码,不能包含多也不能包含少。包含太多可能会影响逻辑,也会降低效率

共享数据:多个线程共同操作的数据

同步监视器:任何一个类的对象,同步监视器也称之为锁。要求,几个线程中的“锁”只能有一个,即同步监视器为同一个对象

补充:

当通过实现Runnable接口来创建线程,也可以把this来当作“锁”,只需要保证多线程中“锁”的唯一性就好

然而是通过继承Thread类的方式创建线程时,一般用static对象,或可以把当前类作为同步监视器,例如:TicketWindow.class

加上线程锁的多线程代码

public class SellTicket {
    public static void main(String[] args) {
        TicketWindow window = new TicketWindow();

        Thread threadFirst = new Thread(window);
        Thread threadSecond = new Thread(window);
        Thread threadThird = new Thread(window);

        threadFirst.setName("窗口一");
        threadSecond.setName("窗口二");
        threadThird.setName("窗口三");

        threadFirst.start();
        threadSecond.start();
        threadThird.start();
    }
}


class TicketWindow implements Runnable{

    private int ticket = 50;
    //private String monitor = "同步监视器";

    @Override
    public void run() {
        while (true){
            //加上同步代码块synchronized(同步监视器){}
            synchronized(this){//monitor
                if (ticket > 0){
                    System.out.println(Thread.currentThread().getName() + ": " + ticket);
                    ticket -- ;
                }else {
                    break;
                }
            }
        }
    }
}

 结果:

给线程加上锁,使得争夺到“锁”对象的线程执行完该线程之前,其他线程处于阻塞状态。

方法二:同步方法

如果多线程中操作共享数据的代码恰好是一个方法,可以使用同步方法的方式解决线程安全问题

public void synchronized 操作共享数据的方法(){
    //操作共享数据的代码
}

public class SellTicket {
    public static void main(String[] args) {
        TicketWindow window = new TicketWindow();

        Thread threadFirst = new Thread(window);
        Thread threadSecond = new Thread(window);
        Thread threadThird = new Thread(window);

        threadFirst.setName("窗口一");
        threadSecond.setName("窗口二");
        threadThird.setName("窗口三");

        threadFirst.start();
        threadSecond.start();
        threadThird.start();
    }
}


//同步方法
class TicketWindow implements Runnable{

    private int ticket = 50;
    @Override
    public void run() {
        while (true){
            show();
        }
    }
    //操作共享数据的方法 在方法名前加上synchronized
    private synchronized void show() {
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + ": " + ticket);
            ticket--;
        }
    }

tips:其实用同步方法解决线程的安全问题也是给线程加了线程锁

在非静态方法上加上synchronized 同步监视器为this(调用该方法的对象) --- 一般用于实现Runnable接口创建的线程

在静态方法上加上synchronized 同步监视器为当前类 --- 一般用于用继承Thread类创建的线程

结果:

 方法三:Lock锁

介绍:

Lock锁是一种更强大的线程同步机制,通过显示定义同步锁的方式来实现同步。同步锁使用Lock对象来充当,Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了共享资源的独家访问

Lock锁不同于以上两种方法,以上两种方法时通过synchronized来实现的,而Lock是一个接口,一般使用其实现类ReentrantLock

ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义

使用:

e.g.

public class LockTest {
    public static void main(String[] args) {
        TicketWindow window = new TicketWindow();

        Thread threadFirst = new Thread(window);
        Thread threadSecond = new Thread(window);
        Thread threadThird = new Thread(window);

        threadFirst.setName("窗口一");
        threadSecond.setName("窗口二");
        threadThird.setName("窗口三");

        threadFirst.start();
        threadSecond.start();
        threadThird.start();
    }
}


//Lock接口 ReentrantLock接口实现类
class TicketWindow implements Runnable{

    private int ticket = 50;

    //实例化ReentrantLock
    //构造器中可以填入参数,false代表不公平,true代表公平,不填写默认为false
    //公平就是先进先出,多个线程有序的执行lock()锁包含的范围
//    ReentrantLock reentrantLock = new ReentrantLock(true);
    ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            //把lock放在try中
            try {
                //上锁
                lock.lock();
                //操作共享数据的代码
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ": " + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                //解锁(解锁方法放在finally中)
                lock.unlock();
            }
        }
    }
}

结果:

tips:使用时需要先创建Lock实现类ReentrantLock的对象,然后再try中通过lock( )方法给操作共享数据的代码上锁,之后在finally中通过unlock( )方法解锁。

方式三和方式一,方式二的区别

相同:

两者都可以解决线程安全问题

不同:

Lock的方式需要 手动启动同步(lock( )方法)手动结束同步(unlock( ))方法,而synchronized机制是在执行完操作共享数据的代码后自动释放同步监视器。即Lock是显式锁,synchronized是隐式锁(离开作用域自动释放)。

Lock只有代码块锁,而synchronized有代码块锁和方法锁

使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并具有更好的扩展性,提供很多实现类

使用的优先顺序:

Lock > 同步代码块 > 同步方法


同步的好处与影响

好处

线程的安全问题得到解决,多个线程不会同时操作共享数据

影响

一个线程对共享数据操作时其他线程阻塞,相当于一个单线程过程,程序执行变慢了一些,效率变低

学习笔记!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值