《JavaEE》第七周day5学习笔记-Thread2

一、线程安全

(一)线程安全概念


在多线程环境下,多个线程可能会同时执行同一段代码,若程序每次运行的结果和单线程的结果是一致的,所有变量的值与预期一致,则说明线程是安全的。
注意,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

(二)线程同步机制


Java提供了线程同步机制(synchronized),来解决多线程环境下的同步问题,具体有三种方式

  • 同步代码块
  • 同步方法
  • 锁机制

1.同步代码块

synchronized(同步锁){
//需要同步的代码
}
同步代码块中的同步锁本质是对象,及多个线程只有获得同步锁对象的线程方可执行同步代码,确保最多只允许一个线程拥有读写权利。
注意,创建长度为0的byte数组对象操作码仅3条,作为同步锁相对节约资源。

2.同步方法

public synchronized void method(){
//需要同步的代码(方法体)
}
同步方法模式下,非静态方法的同步锁为this(与方法所在的类对象相关,若new出两个类对象,则同步方法不能互斥!),静态方法的同步锁为类的字节码对象(类名.class)。
注意,下例反映了在jdk1.8环境下,通过类名.class、对象1.getClass、对象2.getClass三种方式创建的锁是互斥的(同一把锁),网上盛行的“记得在《Effective Java》一书中看到过将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。”说辞有误!

import java.util.Date;

public class Demo4 {

    static int ticket= 1000;
    static Object object = new Object();
    //锁1
    static Class<Date> dateClass = Date.class;
    //锁2
    static Date date = new Date();
    static Class<? extends Date> aClass = date.getClass();
    //锁3
    static Date date1 = new Date();
    static Class<? extends Date> aClass1 = date1.getClass();
    
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {

                while (true) {
                    synchronized (Demo4.dateClass) {
                        if (Demo4.ticket > 0) {
                            ticket = ticket - 1;
                            System.out.println(Thread.currentThread().getName() + "卖出一张票,余票:" + ticket);
                        }
                    }
                    if (Demo4.ticket == 0) {
                        break;
                    }
                }

            }
        }, "线程1");

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {

                while (true) {
                    synchronized (Demo4.aClass) {
                        if (Demo4.ticket > 0) {
                            ticket = ticket - 1;
                            System.out.println(Thread.currentThread().getName() + "卖出一张票,余票:" + ticket);
                        }
                    }
                    if (Demo4.ticket == 0) {
                        break;
                    }
                }

            }
        }, "线程2");

        Thread thread3 = new Thread(new Runnable() {
            @Override
            public void run() {

                while (true) {
                    synchronized (Demo4.aClass1) {
                        if (Demo4.ticket > 0) {
                            ticket = ticket - 1;
                            System.out.println(Thread.currentThread().getName() + "卖出一张票,余票:" + ticket);
                        }
                    }
                    if (Demo4.ticket == 0) {
                        break;
                    }
                }

            }
        }, "线程3");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

3.Lock 锁

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作。
public void lock():加同步锁。
public void unlock() :释放同步锁。

(三)死锁


当多个线程同时阻塞等待某个资源(锁)的释放,由于相互等待使线程被无限期阻塞,不能正常运行或终止,即产生死锁。

1.产生死锁的四个条件

(1)互斥条件:该资源任意一个时刻只由一个线程占用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

2.破坏死锁的四种方式

(1)破坏互斥条件:这个条件较难破坏,因为锁本身就是想让他们互斥的(临界资源需要互斥访问)。
(2)破坏请求与保持条件:一次性申请所有的资源。
(3)破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
(4)破坏循环等待条件:靠按序申请资源来预防,按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

(四)常见线程名称


1.名称概念

主线程:Java虚拟机调用main()产生的线程
当前线程:当前CPU执行的线程,可通过Thread.currentThread()方法获取
后台线程:即守护线程(例如Java虚拟机的垃圾回收线程),依赖主线程结束而结束,可以通过isDaemon()判断是否守护线程和setDaemon()设置守护线程
前台线程:接收后台线程服务的线程,由前台线程创建的线程默认也是前台线程

2.常用方法

sleep():设置线程睡眠(单位:毫秒)
isAlive():判断线程是否存活
join():等待线程结束
activeCount():统计程序中活跃的线程数量
enumerate(): 枚举程序中的线程。
currentThread(): 得到当前线程。
isDaemon(): 一个线程是否为守护线程。
setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
setName(): 为线程设置一个名称。
wait(): 强迫一个线程等待。
notify(): 通知一个线程继续运行。
setPriority(): 设置一个线程的优先级。

二、等待唤醒机制

(一)线程间通信


概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。

比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。

(二)等待唤醒


这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

wait/notify 就是线程间的一种协作机制。

等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

  1. wait:线程不再活动,不再参与调度,进入 wait set(锁池) 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
    wait(long m):wait方法如果在指定的毫秒之后,还没有被notify唤醒,就会自动醒来
  2. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
  3. notifyAll:则释放所通知对象的 wait set 上的全部线程。

(三)生产者与消费者


等待唤醒机制其实就是经典的“生产者与消费者”的问题。
就拿生产包子消费包子来说等待唤醒机制如何有效利用资源:

  • 包子铺线程生产包子,食客线程消费包子。
  • 当包子没有时(包子状态为false),食客线程等待,包子铺线程生产包子(即包子状态为true),并通知食客线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。
  • 接下来,食客线程能否进一步执行则取决于锁的获取情况。
  • 如果食客获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),食客线程进入等待。
  • 包子铺线程能否进一步执行则取决于锁的获取情况。

三、有限状态机

  • NEW
    尚未启动的线程处于此状态。
  • RUNNABLE
    在Java虚拟机中执行的线程处于此状态。
  • BLOCKED
    被阻塞等待监视器锁定的线程处于此状态。
  • WAITING
    正在等待另一个线程执行特定动作的线程处于此状态。
  • TIMED_WAITING
    正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
  • TERMINATED
    已退出的线程处于此状态。

四、线程安全的集合及数据共享

(一)CopyOnWriteArrayList


CopyOnWriteArrayList如何做到线程安全的?

CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。

当有新元素加入的时候,创建新数组,并往新数组中加入一个新元素,这个时候,array这个引用仍然是指向原数组的。

CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。

这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。
由于所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:

  • 如果写操作未完成,那么直接读取原数组的数据;
  • 如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
  • 如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

CopyOnWriteArrayList的读操作是可以不用加锁的。

(二)ConcurrentHashMap


  • 结构上和 Java8 的 HashMap 基本上一样,不过它要保证安全性,所以源码上确实要复杂一些。注意:Java8 的 ConcurrentHashMap 不再使用Java7的 Segment 分段锁来保证并发写,而是使用 CAS 操作来保证线程安全的。
  • CAS 是 compare and swap 的缩写,即我们所说的比较交换。CAS 是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。
  • CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS 是通过无限循环来获取数据的,如果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

(三)ThreadLocal


通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

发布了31 篇原创文章 · 获赞 0 · 访问量 796

猜你喜欢

转载自blog.csdn.net/u010761121/article/details/103930979