• 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.尽量避免同步嵌套
  • 相关阅读:
    C#多态的实现
    C#虚方法
    stm32HAL库中串口部分各个传输和接收函数分析
    ASC字符串取模网址
    STM32F1高级定时器做普通PWM输出配置(例TIM1)
    maven 插件说明
    mac 离线安装yarn
    Tomcat 远程调试
    杀死 tomcat 进程的脚本
    mysql 安装
  • 原文地址:https://www.cnblogs.com/vpersie2008/p/12802376.html
Copyright © 2020-2023  润新知