java多线程:线程同步synchronized(不一样步的问题、队列与锁),死锁的产生和解决


0、不一样步的问题


并发的线程不安全问题:html

多个线程同时操做同一个对象,若是控制很差,就会产生问题,叫作线程不安全。java

咱们来看三个比较经典的案例来讲明线程不安全的问题安全

0.1 订票问题

例如前面说过的黄牛订票问题,可能出现负数或相同。多线程

线程建立方式&&黄牛订票模拟并发

0.2 银行取钱

再来看一个取钱的例子:app

/*
    模拟一个帐户
*/
class Account{
    int money;
    String name;
    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}
/*
    模拟取款机,方便设置名字,继承Thread而不是实现Runnable
*/
class Drawing extends Thread{
    Account account;
    int outMoney;//取出去了多少钱
    int outTotal;//总共取到了多少钱

    public Drawing(Account account, int outMoney,String name) {
        super(name);
        this.account = account;
        this.outMoney = outMoney;
    }

    @Override
    public void run() {
        account.money -= outMoney;
        outTotal += outMoney;
        System.out.println(this.getName() + "---帐户余额为:" + account.money);
        System.out.println(this.getName() + "---总共取到了:" + outTotal);
    }
}

而后咱们写个客户端调用一下,假设两我的同时取钱,操做同一个帐户ide

public class Checkout {
    public static void main(String[] args) {
        Account account = new Account(200000,"礼金");
        Drawing you = new Drawing(account,8000,"你");
        Drawing wife = new Drawing(account,300000,"你老婆");
        you.start();
        wife.start();
    }
}

运行起来,问题就会出现。性能

每次的结果都不同,并且,这样确定会把钱取成负数,显然这是非法的(嘻嘻),首先逻辑上须要修改,当钱少于 0 了就应该退出,而且不能继续取钱的动做了。按照这个思路,加上一个判断呢?学习

if (account.money < outMoney){
    System.out.println("余额不足");
    return;
}
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

但是即使是这样,发现仍是会出现结果为负的状况,没法保证线程安全测试

0.3 数字递增

还有一个经典的例子,那就是对于直接计算迭代过慢,而转为多线程。

一个数字 num ,开辟一万个线程对他作 ++ 操做,看结果会是多少。

public class AddSum {
    private static int num = 0;
    public static void main(String[] args) {
        for (int i=0; i<=10000; i++){
            new Thread(()->{
                num++;
            }).start();
        }
        System.out.println(num);
    }
}

每次运算的结果都不同,同样的是,结果永远 < 10000 。

或者用给 list 里添加数字来测试:

List<String> list = new ArrayList<>();
for (int i=0; i<10000; i++){
    new Thread(()->{
        list.add(Thread.currentThread().getName());
    }).start();
}
System.out.println(list.size());

同样的结果。

线程不安全的问题如何解决呢?


1、同步(synchronized)


1.1 问题出现的缘由

从前面的介绍里,咱们总结出会出现同步问题的状况,也就是并发三要素:多个线程、同时操做、操做同一个对象。另外,操做的特色是:操做类型为修改这个时候会产生并发的问题,线程安全问题。

1.2 解决方案

  1. 确保线程安全,第一就是排队。只要排队,那么无论多少线程,始终一个时间点只会有一个线程在执行,就保证了安全。
    不过排队会有一个问题:怎么直到轮到我了呢,也就是怎么知道排在前面的线程执行完了呢?
  2. 现实生活中,可能会用相似房卡的形式,前一我的把卡交还了,才会有后面的人有机会入住。这就是

利用 队列 + 锁 的方式保证线程安全的方式叫线程同步,就是一种等待机制,多个同时访问此对象的线程进入这个对象的等待池 造成队列,前面的线程使用完毕后,下一个线程再使用。

锁机制最开始在 java 里就是一个关键字 synchronized(同步),属于排他锁,当一个线程得到对象的排他锁,独占资源,其余线程必须等待,使用后释放锁便可。

按照这种思路,能够想象到这种保证安全方式的弊端,也就是早期的 synchronized 存在的问题:

  1. 一个线程持有锁会致使其余全部须要这个锁的线程挂起;
  2. 多线程竞争下,加锁、释放锁致使耗时严重,性能问题
  3. 一个优先级高的线程等待一个优先级低的线程的锁释放,会使得本应该的优先级倒置,引发性能问题。

另外,Synchronized 是基于底层操做系统的 Mutex Lock 实现的,每次获取和释放锁操做都会带来用户态和内核态的切换,从而增长系统性能开销。所以,在锁竞争激烈的状况下,Synchronized 同步锁在性能上就表现得很是糟糕,它也常被你们称为重量级锁。

可是 jdk 6 以后有了很强的改进,这个内容待更新,留个坑。


2、同步关键字的用法


2.1 同步方法

synchronized 方法控制对 成员变量或者类变量 对象的访问,每一个对象对应一把锁。写法以下:

public synchronized void test(){
    //。。。
}
  1. 若是修饰的是具体对象:锁的是对象
  2. 若是修饰的是成员方法:那锁的就是 this
  3. 若是修饰的是静态方法:锁的就是这个对象.class

每一个 synchronized 方法都必须得到调用该方法的对象的锁才能执行,不然所属的这个线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时,锁释放。

同步方法的写法代码,以上面的取钱案例历的 取钱类为例,若是直接在提款机的操做,把 run 方法或者里面的内容提出来变成 test ,加上 synchronized 修饰:

@Override
public void run() {
    test();
}
public synchronized void test(){
    //内容都不变
}

会发现,仍然出现了负数。锁定失败。

分析

咱们认为在 test 方法里进行的对象修改,因此把他锁上就行了,可是对于这个类,这个提款机类来讲,test 方法是成员方法,所以锁的对象其实是 this ,也就是提款机。

但咱们的初衷,要线程锁的资源应该是 Account 对象,而不是提款机对象。

2.2 同步块

除了方法,synchronized 还能够修饰块,叫作同步块

synchronized 修饰同步块的方式是:

synchronized (obj){
    //...
}

其中的 obj 能够是任何对象,可是用到它,确定是设置为那个共享资源,这个 obj 被称为同步监视器同步监视器的做用就是,判断这个监视器是否被锁定(是否能访问),从而决定是否能执行其中的代码。

java的花括号中内容有如下几种:

  1. 方法里面的块:局部块。解决变量做用域的问题,快速释放内存(好比方法里面再有个for循环,里面的变量);
  2. 类层的块:构造块。初始化信息,和构造方法是同样的;
  3. 类层的静态块:静态构造快。最先加载,不是对象的信息,而是类的信息;
  4. 方法里面的同步块:监视对象。

第四种就是咱们这里学习的同步块。

注意,若是是同步方法里,不必指定同步监视器,由于同步方法的监视器已是 this 或者 .class。

用同步块的方式对提款机问题进行修改:

public void test(){
    synchronized(account){
            //内容不变
    }
}

也就是加上对 account 的监视器,锁住这个对象。这样运行结果就正确了 。

这种作法效率不高,由于虽然对 account 上了锁,可是每一次都要把整个流程走一遍,方法体的内容是不少的,另外,每次加锁与否,都是性能的消耗,进入以后再出来,哪怕什么也不作,也是消耗。

其实,咱们能够在加锁的前面再加一重判断,那么以后就不必再进行上锁的过程了。

public void test(){
    if (account.money ==0 ){
        return;
    }
    synchronized(account){
    }
}

就是这样的一个代码,在并发量很高的时候,每每能够大大提升效率

对于上面的 10000 个线程的加法那个问题,咱们也能够经过 synchronized 加锁,来保证结果的正确性。

(可是 synchronized 修饰的要是引用类型,因此直接对 int num 加锁不行,通常直接使用专门提供的原子类)

list 的里加数字的测试:

List<String> list = new ArrayList<>();
for (int i=0; i<10000; i++){
    new Thread(()->{
        synchronized (list){
            list.add(Thread.currentThread().getName());
        }
    }).start();
}
Thread.sleep(2000);
System.out.println(list.size());

main方法,下面的print语句,这些都是线程,因此可能上面尚未操做的时候,就已经输出了,为了方便观察,咱们在最后输出以前先让main线程休眠一会,再看里面add的结果是否正确。

tips:对于容器的操做,Java的util.concurrent包里也直接提供了对应的安全容器CopyOnWriteArrayList。

CopyOnWriteArrayList<String > list1 = new CopyOnWriteArrayList<>();
for (int i=0; i<10000; i++){
    new Thread(()->{
        list1.add(Thread.currentThread().getName());
    }).start();
}
Thread.sleep(2000);
System.out.println(list1.size());

2.3 问题

synchronized 块过小,可能锁不住,安全性又不行了,锁的方法太大,又效率会下降,因此要很注意控制范围

并且,还有相似于 单例模式 里 Double-Check 写法针对的问题,有时候一重锁性质不够,两重锁仍然不够保证安全。


3、线程同步问题应用示例


3.1 快乐影院

电影院买票。

/**
* 快乐影院
*/
public class HappyCinema {
    public static void main(String[] args) {
        Cinema cinema = new Cinema(20, "万达");
        new Thread(new Customer(cinema,2)).start();
        new Thread(new Customer(cinema,1)).start();
    }
}
/**
* 电影院,提供订票方法
*/
class Cinema{
    int available;
    String name;

    public Cinema(int available, String name) {
        this.available = available;
        this.name = name;
    }
    //提供购票方法
    public boolean bookTickets(int seats){
        System.out.println("可用位置为:"+available);
        if (seats > available){
            return false;
        }
        available -= seats;
        return true;
    }
}
/**
* 顾客,有多个顾客,模仿多线程
*/
class Customer implements Runnable{
    Cinema cinema;
    int seats;
    //顾客建立的时候带上要预约的做为+订哪一个影院
    public Customer(Cinema cinema, int seats) {
        this.cinema = cinema;
        this.seats = seats;
    }

    @Override
    public void run() {
        boolean flag = cinema.bookTickets(seats);
        if (flag){
            System.out.println("出票成功,"+Thread.currentThread().getName()+"买了 "+seats+" 张票");
        }else{
            System.out.println("出票失败,"+Thread.currentThread().getName()+"买票,但位置不足 ");
        }
    }
}

对于一个电影院的票:available 资源来讲,多个线程访问,是须要同步的,不然就会出现不安全的问题。

解决:

@Override
public void run() {
    synchronized (cinema){
                //。。。
        }
    }
}

3.2 快乐影院进阶

影院票的时候不是简单计数,是能够选座位的,咱们修改代码,具体到某一个座位号的预约。

将 int 座位数目改为 List,那么购票方法改动以下:

public boolean bookTickets(List<Integer> seats){
        System.out.println("可用位置为:" + available);
        List<Integer> copy = new ArrayList<>(available);
        //相减
        copy.removeAll(seats);
        //判断改变后
        if (available.size() != copy.size() + seats.size() ){
            return false;
        }
        available = copy;
        return true;
    }

其余地方只须要作简单的修改,在调用的时候传入一个构造好的 list 便可,这个时候再来看:

若是两个顾客同时订票的位置冲突

能够看到完成了同步。

3.3 火车票

仍是相似于订票,由于上面电影院的部分咱们都使用 同步块 的方式锁定某个对象,这里使用同步方法来加深上锁的理解。

模仿第一种电影院订票的初始不加锁写法。

public class Happy12306 {
    public static void main(String[] args) {
        Railway railway = new Railway(20, "京西G12138");
        new Thread(new Passenger(railway,2)).start();
        new Thread(new Passenger(railway,1)).start();
    }
}
/**
* 铁路系统,提供订票方法
*/
class Railway{
    int available;
    String name;

    public Railway(int available, String name) {
        this.available = available;
        this.name = name;
    }
    //提供购票方法
    public boolean bookTickets(int seats){
        System.out.println("可用位置为:"+available);
        if (seats > available){
            return false;
        }
        available -= seats;
        return true;
    }
}
/**
* 顾客,有多个顾客,模仿多线程
*/
class Passenger implements Runnable{
    Railway railway;
    int seats;
    public Passenger(Railway railway, int seats) {
        this.railway = railway;
        this.seats = seats;
    }

    @Override
    public void run() {
        boolean flag = railway.bookTickets(seats);
        if (flag){
            System.out.println("出票成功,"+Thread.currentThread().getName()+"买了 "+seats+" 张票");
        }else{
            System.out.println("出票失败,"+Thread.currentThread().getName()+"买票,但位置不足 ");
        }
    }
}

如今开始给方法加锁,考虑这个问题:

  1. 原本的 run 方法写了 同步块 对一个资源加锁,这个资源是 票所在的 铁路系统(上一个例子的电影院);
  2. 因此若是锁 run 方法,咱们前面说过的,锁成员方法至关于锁的 this,也就是锁了 乘客 类,是没有用的,由于被修改的资源不在这里
  3. 应该将这个方法放到 铁路系统 类里,而后对这个方法上锁。

这样会带来新的问题,模拟多个线程的线程体应该来源于 乘客 ,不能是铁路系统,因此乘客类也要继续修改,继承 Thread 类,自己做为一个代理,去找到目标接口的实现类:铁路系统 ,而后start。

public class Happy12306 {
    public static void main(String[] args) {
        Railway railway = new Railway(5, "京西G12138");
        new Passenger(5,railway,"乘客B").start();
        new Passenger(2,railway,"乘客A").start();
    }
}
/**
* 铁路系统,提供订票方法,自己就是一个线程,
*/
class Railway implements Runnable{
    int available;
    String name;

    public Railway(int available, String name) {
        this.available = available;
        this.name = name;
    }
    //提供购票方法,加入同步
    public synchronized boolean bookTickets(int seats){
        System.out.println("可用位置为:"+available);
        if (seats > available){
            return false;
        }
        available -= seats;
        return true;
    }
    //run方法从 顾客类里 挪过来,
    @Override
    public void run() {
        //运行时须要知道哪一个线程在操做本身,也就是seats的来源
        Passenger p = (Passenger) Thread.currentThread();
        boolean flag = this.bookTickets(p.seats);
        if (flag){
            System.out.println("出票成功,"+Thread.currentThread().getName()+"买了 "+p.seats+" 张票");
        }else{
            System.out.println("出票失败,"+Thread.currentThread().getName()+"买票,但位置不足 ");
        }
    }
}
/**
* 顾客,做为代理,是 Thread 的子代理
*/
class Passenger extends Thread{
    int seats;
    public Passenger(int seats, Runnable target, String name) {
        super(target,name);//用父类方法找到目标,也就是铁路系统
        this.seats = seats;
    }
}

总结:

  1. synchronized 修饰成员方法锁定的是 this,因此要加入铁路系统类,
  2. 铁路系统经过 Thread.currentThread() 方法 肯定当前的线程,同时获取到订票信息
  3. 乘客变成了 代理,是 Thread 的子类,在这个基础上加入订票信息
  4. 最后调用的时候,本应该使用 Thread 做为代理去执行,改成用乘客类,起到了一个系统用多个不一样线程的做用

乘客自己做为代理子类可能比较难理解。

可是咱们回头看看,对于上一种方式:

new Thread(new Passenger(railway,2)).start();
        new Thread(new Passenger(railway,1)).start();

虽然这么写的,可是其实传入的一个 Runnable的实现类,在 Thread 源码里面调用了构造方法:

能够看到,传入一个 Runnable ,这个构造器加上了额外的信息,因此其实咱们这种作法:

public Passenger(int seats, Runnable target, String name) {
    super(target,name);//用父类方法找到目标,也就是铁路系统
    this.seats = seats;
}

是模拟了源码的写法而已。


4、多线程死锁的产生与解决


4.1 问题

死锁:当多个线程各自占有一些共享资源,而且互相等待其余线程占有的资源才能进行,从而致使两个或者多个线程都在等待对方释放资源,都中止执行的状况。

最简单的,某一个同步块同时拥有“两个以上的对象的锁”的时候,就可能会发生死锁问题。

  • 若是两个线程,那就是涂口红、照镜子的问题,每一个人都想先拿了一个再拿另外一个;
  • 若是是多个线程,对应哲学家就餐问题,每一个人都想左手拿刀、右手拿叉。

口红镜子问题示例:

/**
* 死锁的产生
*/
public class DeadLock {
    public static void main(String[] args) {
        Makup makup = new Makup(1,"女孩1");
        Makup makup1 = new Makup(0,"女孩2");
        makup.start();
        makup1.start();
    }
}
/**
* 口红
*/
class Lipstick{ }
/**
* 镜子
*/
class Mirror{ }
/**
* 化妆
*/
class Makup extends Thread{
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();
    int choice;//选择
    String girl;
    public Makup(int choice, String girl){
        this.choice = choice;
        this.girl = girl;
    }

    @Override
    public void run() {
        makeup();
    }
    //相互持有对方的对象锁
    private void makeup(){
        if (choice == 0){
            synchronized (lipstick){
                System.out.println(this.girl + "得到口红");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (mirror){
                    System.out.println(this.girl + "而后得到镜子");
                }
            }
        }else{
            synchronized (mirror){
                System.out.println(this.girl + "得到镜子");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lipstick){
                    System.out.println(this.girl + "而后得到口红");
                }
            }
        }
    }
}

能够发现程序停不下来了,死锁已经产生。

其中的过程就是:

  1. 女孩 1 先拿到了镜子,对其上锁;
  2. 女孩 1 休息的时候,女孩 2 先拿到了口红,对其上锁;
  3. 女孩 2 休息的时候,女孩 1 休息结束,想要获取口红,但此时口红上锁,所以等待;
  4. 女孩 2 休息结束,想要获取镜子,但此时镜子上锁,所以等待。

4.2 解决

解决这个问题的方法:

不要出现 锁的 嵌套 ,将等待后获取另外一个锁的代码放到第一个加锁的后面就能够解决这个问题了:

private void makeup(){
        if (choice == 0){
            synchronized (lipstick){
                System.out.println(this.girl + "得到口红");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (mirror){
               System.out.println(this.girl + "而后得到镜子");
            }
        }else{
            synchronized (mirror){
                System.out.println(this.girl + "得到镜子");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (lipstick){
                System.out.println(this.girl + "而后得到口红");
            }
        }
    }

总结:尽可能不要让 一个同步代码块 同时拥有“两个以上的对象的锁”。

相关文章
相关标签/搜索