5、线程安全问题

5.1共享资源的冲突

案例1:

        有几个人围着桌子吃蛋糕,当你举起叉子,准备叉起最后一块蛋糕,正当叉子碰到蛋糕的时候才发现它已经被另一个叉子叉起。这类现象就是共享资源(蛋糕)中出现的冲突,而两个人同时用叉子叉同一块蛋糕的过程我们就称为并发。

案例2:

        转账案例:

5.2问题分析

线程安全问题都是由全局变量及静态变量引起的。

若每个线程对全局变量、静态变量只读,不写,一般来说,这个变量是线程安全的;

若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

综上所述,线程安全问题根本原因: 

 多个线程在操作共享的数据

5.3问题解决-线程同步

专业:即线程操作共享数据时其他线程不可以参与执行,直到该线程完成操作;

民间:同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说;

为了保证每个线程都能正常执行共享资源操作,Java引入了7种线程同步机制:

        1)同步代码块(synchronized

        2)同步方法(synchronized

        3)同步锁(ReenreantLock

        4)特殊域变量(volatile

        5)局部变量(ThreadLocal

        6)阻塞队列(LinkedBlockingQueue

        7)原子变量(Atomic

​​​​​​​同步代码块synchronized):

        synchronized  关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

语法:

        synchronized(钥匙){

        需要同步操作的代码

        }

        钥匙:钥匙可以是任意类型;多个线程要使用同一把钥匙。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

​​​​​​​同步方法synchronized):

        使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

格式:

        public synchronized void method(){

        可能会产生线程安全问题的代码

        }

同步锁是谁?

        对于非static方法,同步锁就是this

        对于static方法,同步锁是当前方法所在类的字节码对象(类名.class)

​​​​​​​同步锁ReentrantLock):

        java.util.concurrent.locks.Lock 提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

方法:

        public void lock() :加同步锁。

        public void unlock() :释放同步锁。

案例:

package com.hg.Thread.lock;

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

public class TicketTest {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(ticket,"窗口-1").start();
        new Thread(ticket,"窗口-2").start();
        new Thread(ticket,"窗口-3").start();

    }
}

class Ticket implements Runnable{
    private Integer ticketNum =10;
    private Object obj = new Object(); //唯一钥匙
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            sellTicket();
            }
        }
        //方式二:同步方法,此时this是钥匙
    private /*synchronized*/ void sellTicket(){
        //方式一:同步代码块
        //synchronized (obj){ //锁
        lock.lock(); //方式三:和同步代码块一样
        try {
            if (ticketNum > 0){
                System.out.println("当前" + Thread.currentThread().getName() + "售票:" + ticketNum--);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }finally {
            lock.unlock();
        }
    }
}

6、死锁

6.1什么是死锁

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

6.2产生死锁的原因

 线程 1 持有r1,等待获取r2;

 线程 2 持有r2,等待获取r1;

 两个线程都不会释放已持有的锁,且都在等待对方的锁,形成循环等待,最终导致死锁。

6.3怎样避免死锁

  避免一个线程同时获取多个锁;

7、线程通讯

7.1​​​​​​​为什么要线程通信

多个线程并发执行时,在默认情况下CPU是随机切换线程的,有时我们希望CPU按我们的规律执行线程,此时就需要线程之间协调通信。

7.2方式

        常用方法:

        wait():一旦执行此方法,当前线程就进入阻塞状态,并释放cpu资源。

        notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。

        notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

案例:两个线程交替打印1-100

package com.hg.Thread.Communication;

public class CommTest {
    public static void main(String[] args) {
        Comm comm = new Comm();
        new Thread(comm).start();
        new Thread(comm).start();
    }
}

class Comm implements Runnable{
    private int i = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                this.notify(); //唤醒等待此钥匙的线程
                if (i <= 100) {
                    System.out.println(Thread.currentThread().getName() +": " + i++);
                }else {
                    break;
                }
                try {
                    this.wait(); //该线程让出钥匙并进入阻塞(等待)状态(让出钥匙后必须被notify(),否则一直拿不到钥匙)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

8、线程的生命周期

8.1新建

        new关键字创建了一个线程之后,该线程就处于新建状态
        JVM为线程分配内存,初始化成员变量值

8.2就绪

        当线程对象调用了start()方法之后,该线程处于就绪状态
        JVM为线程创建方法栈和程序计数器,等待线程调度器调度

8.3运行

        就绪状态的线程获得CPU资源,开始运行run()方法,该线程进入运行状态

8.4阻塞

        在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中
        止自己的执行,进入阻塞状态

8.5死亡

线程会以如下3种方式结束,结束后就处于死亡状态:
        run()或call()方法执行完成,线程正常结束。
        线程抛出一个未捕获的Exception或Error。
        调用该线程stop()方法来结束该线程,该方法容易导致死锁,不推荐使用。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐