陕西队西北狼

Java 多线程二、线程的生命周期、线程安全、死锁

一、 线程的生命周期

线程是存在生命周期的,线程从创建之后,运行后执行完相关操作,其终点一定是死亡。

如下图:演示线程的生命周期:

线程的生命中期分为五个阶段

  • 1.新建
  • 2.就绪
  • 3.运行
  • 4.阻塞(不一定有
  • 5.死亡

这5个阶段里,其中阻塞是不一定有的,其他几个状态都有,线程的最终结果都是死亡。

1.正常状态的变化:

  • 新建->就绪:
    调用thread.start() 方法即可让线程到就绪状态。
  • 就绪->运行:
    当线程获取CPU的执行权时,即会到运行状态。
  • 运行->就绪:
    当运线程行时,失去CPU的执行权,即会到就绪状态,如使用yeild()方法强制切换CPU的执行权。
  • 运行->死亡:
    线程的最终结果都应该是死亡,从运行到死亡是不可逆的。
    通过执行stop() 方法,或者是抛异常(Error/Exception)没有处理的情况下,则该线程会死亡。

2.有阻塞状态的变化

  • 运行->阻塞
    通过sleep(time)方法调用,让线程等待一段时间,此时线程会到阻塞状态,直到sleep时间到。
    通过wait() 让线程挂起,直到有notify() 通知该线程重新到就绪状态。

  • 阻塞->就绪
    首先明确,阻塞不能直接到运行状态,线程阻塞后,当线程被唤醒时,他是先到就绪状态,当该线程拿到CPU 的执行权时,放可到运行状态。

    • 1).当sleep 时间到时,会从阻塞到就绪
    • 2).当被wait的线程,使用 notify/notifyAll 方法唤醒线程时,被wait的线程会到就绪状态。

二、 线程的安全问题

当多线程之间有共享数据的时候,就会存在线程安全问题。

多线程导致错票问题

  • 1.卖票过程出现了重票,错票问题,即出现了线程安全问题。
  • 2.出现重票和错票的原因,是因为在一个线程还没操作完,另一个线程过来再操作共享数据,即会出现线程安全问题。

例如:

三个线程同时抢100张票,当加了sleep 之后,出现重票,错票的概率大大增加了,因为在上一个线程还没进行ticket--时候,下一个线程也进来了,导致因为共享数据问题导致错票。

代码如下:

public class WindowTest {
    public static void main(String[] args) {
        Window win = new Window();
        Thread t1 = new Thread(win);
        Thread t2 = new Thread(win);
        Thread t3 = new Thread(win);
        t1.start();
        t2.start();
        t3.start();
    }
}

class Window 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--;
            } else {
                break;

            }
        }
    }
}

运行结果中有票号为-1,即错票。

如何解决?
思路:当一个线程在操作共享数据时,当前线程锁定资源,其他线程不能操作该共享数据,这种情况即使该线程阻塞,其他线程也不允许操作共享数据。

三种方式来解决线程安全问题

1.同步代码块

	synchronized(同步监视器){
	//需要被同步的代码
	}
  • 什么是需要被同步的代码?即操作共享数据的代码
  • 什么是共享数据?多个线程共同操作的变量。比如该例子中的ticket
  • 什么是同步监视器?形象的说就是“锁”。任何一个类的对象,都可以充当锁,但切记锁是惟一的。

切记:多个线程公用一把锁,锁是唯一的。

说明:加了同步监视器之后,在同步代码块中,智能有一个线程操作,其他线程等待,所以这块相当于单线程的,所以效率比较低一些。
在使用继承方式来创建多线程时,要慎用this来充当同步监视器,考虑使用当前类充当同步监视器,如 : Window2.class

例如:

如下方式,使用同步代码块方式解决线程安全问题:

public class WindowTest1 {
    public static void main(String[] args) {
        Window1 win = new Window1();
        Thread t1 = new Thread(win);
        Thread t2 = new Thread(win);
        Thread t3 = new Thread(win);
        t1.start();
        t2.start();
        t3.start();
    }
}


class Window1 implements Runnable {
    private int ticket = 100;
    //创建一个唯一对象,做为锁,锁可以使任何对象,但切记锁智能有一把
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            //这里也可以用this 替换obj,因为我们只new了一个Window
            synchronized (obj) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

此时,运行结果中就不会有重票出现了

2.同步方法

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

如下代码演示使用同步方法来解决线程安全问题:

public class WindowTest3 {
    public static void main(String[] args) {
        Window3 win = new Window3();
        Thread t1 = new Thread(win);
        Thread t2 = new Thread(win);
        Thread t3 = new Thread(win);
        t1.start();
        t2.start();
        t3.start();
    }
}


class Window3 implements Runnable {
    private int ticket = 100;

    //创建一个唯一对象,做为锁,锁可以使任何对象,但切记锁智能有一把
    @Override
    public void run() {
        while (true) {
            this.show();
            if (ticket <= 0) {
                break;
            }
        }
    }

    private synchronized void show() {//同步监视器中,这块默认的锁就是this
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

此时,错票数据就不会出现了

注意: 因为此时只有一个Window3对象,所以同步监视器默认就是this

3.Lock 锁 ReentrantLock

JDK 5.0 中新增了可以使用Lock 锁方式解决线程安全问题。

如下代码演示如何通过Lock锁方式,手动加锁和解锁

程序在run方法中,先手动加锁,保证只有当前线程拿到锁后其他线程不会进来,当执行到finally时候,释放锁,这时其他线程才能进来。

package com.jerry.thread6;
import java.util.concurrent.locks.ReentrantLock;

public class WindowTest5 {
    public static void main(String[] args) {
        Window5 win = new Window5();
        Thread t1 = new Thread(win);
        Thread t2 = new Thread(win);
        Thread t3 = new Thread(win);
        t1.start();
        t2.start();
        t3.start();
    }
}


class Window5 implements Runnable {
    private int ticket = 100;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                //手动加锁
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            } finally {
                //手动解锁
                lock.unlock();
            }
        }
    }
}

总结:

面试题一:synchronized 和 lock方式,有什么不同?

相同点: 二者都可以解决线程安全问题。
不同点: lock 方式,是手动加锁和解锁。synchronized 是自动解锁的,他是在执行完同步方法或同步代码块才会自动释放同步监视器(自动解锁)。手动加锁,解锁,更加灵活

面试题二:如何解决线程安全问题?

1)加同步代码块synchronized 关键字

2)使用同步方法 ,即加synchronized 关键字的方法

3)使用 ReentrantLock 手动加锁,手动解锁

单例中的线程安全

如下:

懒汉式代码,存在线程安全问题,因为当多线程创建单例时,可能会重复创建,解决懒汉式线程安全,一般有两种方法

2. 性能较低的懒汉式

这段代码,当第一次创建实例后,其他线程还要去判断锁,性能是比较低的。

class Bank1 {
    private Bank1() {
    }

    private static Bank1 instance = null;

    //这种写法,效率较低,因为第一次已经将instance创建出来了,其他线程再去用的时候,还得去再判断一下锁,效率比较低
    public static Bank1 getInstance() {
        synchronized (Bank1.class) {
            if (instance == null) {
                return new Bank1();
            }
            return instance;
        }
    }
}

1. 性能较高的懒汉式

双重判断,当第一个线程创建了实例对象之后,第二个线程看到他不是NULL 了,就不用去再次判断锁了

class Bank2 {
    private Bank2() {
    }

    private static Bank2 instance = null;

    //这种写法,做了双重判断,当第一个线程创建了实例对象之后,第二个线程看到他不是NULL 了,就不用去再次判断锁了
    public static Bank2 getInstance() {
        if (instance == null) {
            synchronized (Bank2.class) {
                if (instance == null) {
                    return new Bank2();
                }
            }
        }
        return instance;
    }
}

三、 线程的死锁问题

什么是死锁?

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

打个比方:

两个人分别有两双筷子,但是拿的都是对方的筷子,都在等对方把筷子给自己才能去吃饭,这时两个人就僵住了,谁也吃不到饭,这个问题就类似死锁问题。

如下代码演示死锁

如下代码,使用了双重锁,第一个线程,先抓s1,再抓s2,第二个线程,先抓s2,再抓s1。
当线程1执行到sleep时候,挂起了一会,此时线程2刚好运行后抓住s2 的锁不放,因为s2这块也有sleep,这时线程1要去拿s2就拿不到,此时即出现了死锁。

public class DeadLockTest {
    //死锁问题
    //产生死锁的原因,sleep之后,死锁概率加大,两个锁互相等待对方释放锁,而又拿着对方的锁不放,就导致死锁
    public static void main(String[] args) {

        StringBuilder s1 = new StringBuilder();
        StringBuilder s2 = new StringBuilder();
        //使用继承方式创建线程
        new Thread() {
            @Override
            public void run() {
                synchronized (s1) {
                    s1.append("a");
                    s2.append("1");
                    sleepTime(100);

                    synchronized (s2) {
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        //使用实现Runnable 接口的方式创建线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2) {
                    s1.append("c");
                    s2.append("3");
                    sleepTime(100);
                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }

    private static void sleepTime(int time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

怎样避免死锁?

  • 1.专门的算法,原则避免。
  • 2.尽量减少同步资源的定义
  • 3.尽量避免同步嵌套
posted @ 2020-04-29 15:16  PS-Jerry  阅读(224)  评论(0编辑  收藏  举报