首页 > 代码库 > Java-Review-Note——4.多线程

Java-Review-Note——4.多线程

Java-Review-Note——4.多线程


PS:干货满满,看得够呛,本来是分开三篇的,后来想想还是整理成一篇了,只能说,纸上得来终觉浅,绝知此事要躬行,
开了眼界,但很多东西还需要实践…Σ(⊙▽⊙”a 附上静心大悲咒帮助阅读~

程序,进程,线程及多线程的理解

  • 程序:为了完成特定任务,用某种语言编写的一组指令集合(一组静态代码)
  • 进程运行中的程序,系统调度与资源分配的一个独立单位,操作系统会为每个进程分配
    一段内存空间;程序的依次动态执行,经历代码的加载,执行,执行完毕的完整过程。
  • 线程:进程的子集,比进程更小的执行单位,每个进程可能有多条线程,
    线程需要放在一个进行中才能执行线程由程序负责管理而进程则由系统进行调度
  • 多线程的理解并行执行多条指令,将CPU时间片按照调度算法分配给各个线程,
    实际上是分时执行,只是切换的时间很短,用户感觉到”同时”而已。

线程的生命周期

技术分享

注意:可以调isAlive()判断线程是否死亡,如果对已死亡的线程调start()方法会抛出
IllegalThreadStateException异常!


创建线程的三种方式


1.继承Thread类创建

流程:继承Thread类,重写run()方法,实例化线程对象,调start()方法从而启动run()方法。

示例代码

class MyThread extends Thread {
    public MyThread() {
        super("MyThread");  //标识进程名
        System.out.println("新建了线程:" + getName()); //getName()方法可获得线程名
    }
    public void run() { 
        for(int i = 1;i <= 3;i++) {
            System.out.println(Thread.currentThread().getName() +" : " + i );  
            try {  sleep(1000);  } catch (InterruptedException e) {  e.printStackTrace();  }  
        }
    }
}
//测试类
public class ThreadTest1 {
    public static void main(String[] args) {
        Thread t = Thread.currentThread();
        MyThread mt = new MyThread();
        mt.start();
        //这里演示的是线程运行完猴才打印主线程中的语句,join()让异步执行的线程
        //改成同步执行,直到这个线程退出,程序才会继续执行
        try { mt.join(); }catch(InterruptedException e){ e.printStackTrace(); }
        System.out.println(t.getName() + " 打印完毕!");
    }
}

运行结果

技术分享


2.实现Runnable接口创建

流程:实现Runnable接口,覆盖run()方法,实例化自定义线程类对象,实例化Thread对象,
将自定义线程对象作为参数传给Thread对象,调用Thread对象的start()方法启动run()方法。

示例代码

class MyThread implements Runnable {
    public void run() {
        for(int i = 1;i <= 3;i++) {
            System.out.println(Thread.currentThread().getName() +" : " + i );
            try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
        }
    }
}

public class ThreadTest1 {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyThread(),"MyThread");
        thread.start();
        System.out.println(Thread.currentThread().getName() + " 打印完毕!");
    }
}

运行结果

技术分享


3.实现Callable泛型接口创建

流程:实现Callable泛型接口,重写call()方法,然后使用Future或FutureTask来创建。
简述:Callable和Future是jdk 1.5后引入的,引入目的是:解决两种普通创建
Thread无法直接获取执行结果的缺陷;Callable接口中只声明了一个call()的方法,
返回类型就是Callable传进来的V类型,一般情况下是配合ExecutorService来使用。
Future接口是对于具体的Runnable或Callable任务的执行结果进行取消(cancel),
查询是否取消成功(isCancelled),查询是否完成(isDone),获取查询结果(get)
该方法会阻塞直到任务返回结果,另外get还有一个方法get(long timeout, TimeUnit unit),
如果在指定时间内没获取到结果,就会返回null,也就是说Future提供三种功能:
判断任务是否完成,能否中断任务,能够获取任务的执行结果。
因为Future是一个接口,无法直接用来创建对象,因此有了FutureTask。
FutureTask,实现了RunnableFuture接口,而RunnableFuture接口接口继承了
Runnable和Future接口,所以FutureTask既可以作为Runnable被线程执行,又可以作为
Future得到Callable的返回值。

使用示例

实现Callable接口

class MyThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("子线程正在计算...");
        int sum = 0;
        for(int i = 0;i < 100;i++) { sum += i; }
        return sum;
    }
}

Future :

    ExecutorService executor = Executors.newCachedThreadPool();
    MyThread myThread = new MyThread();
    Future<Integer> result = executor.submit(myThread);
    executor.shutdown();    //如果不调shutdown方法,executor会一直等待运行,即使没线程
    try { 
        System.out.println("result运行结果:" + result.get()); 
        } catch (InterruptedException e) { e.printStackTrace();
        } catch (ExecutionException e) { e.printStackTrace(); 
    }

FutureTask

    //第一种ExecutorService
    ExecutorService executor = Executors.newCachedThreadPool();
    MyThread thread = new MyThread();
    FutureTask<Integer> result = new FutureTask<>(thread);
    executor.submit(result);
    executor.shutdown();
    //第二种Thread
    MyThread myThread = new MyThread();
    FutureTask<Integer> futureTask = new FutureTask<>(myThread);
    Thread thread = new Thread(futureTask);
    thread.start();
    try {
        System.out.println("result运行结果:" + result.get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }

运行结果

技术分享


多线程集锦

1.线程执行的顺序

多个线程同时执行又叫线程并发,如果当前有多个线程在并发执行的话,多次运行
的结果可能是不唯一的,Java对于线程启动后唯一能保证的就是:
每个线程都被启动并且结束,但对于哪个线程先执行,何时执行,都没保证。

2.线程的优先级

操作系统中的线程是具有优先级,Java运行环境采用的是固定优先级调度算法
(Fixed Priority Scheduling)某一时刻有多个线程在运行,JVM会选择优先级最高
的线程运行,优先级较高的线程会抢占cpu时间,相同优先级的线程可能顺序执行,
也可能分时执行,取决于本地操作系统的线程调度策略,虽然说在任意时刻,应该
是最高优先级的线程在运行,但是这样是不能保证的,调度程序有可能选择优先级
较低的线程以避免饥饿(starvation)抢占(pre-empt)策略
当一个优先级更高的进程进行可运行状态时发生抢占,终止当正在运行的进程而立即
去执行优先级更高的线程,而两个相同优先级的线程则采用循环执行策略(round-robin);

3.Java中的线程优先级

Java中线程的优先级从0-10,整数,数值越大,优先级越大,默认线程优先级为5,
可调用:setPriority()改变线程优先级,或调用:getPriority()获得线程的优先级,
另外Java还提供了几个线程优先级的字段供使用:
MIN_PRIORITY(最低优先级0);MAX_PRIORITY(最高优先级10);NORM_PRIORITY(默认优先级5)
另外,只能说优先级高的线程更有可能获得CPU,但也不能说优先级较低的线程就
永远最后执行,这不是绝对的,可以理解成设置进程优先级只是给系统提供一个参考

4.Java提供的进程协作相关的方法

使用优先级不能保证并发执行的线程顺序,但是Java也给我们提供了一些线程协作相关的方法:

Thread类中

  • run():线程执行业务逻辑的地方,想让线程执行的代码就写在这里。
  • start():线程开始执行,调用线程对象中的run()方法,只能调用一次,多次启动
    同一个线程,会抛出IllegalThreadStateException异常。
  • sleep():让当前线程休眠(堵塞)一段时间,但不释放持有资源(对象锁),如果此时
    的线程已经被别的经常中断的话,会抛出InterruptedException异常,另外调用该方法时
    需要对异常进行捕获。
  • join():在一个线程中调用第二个线程的join方法,会导致当前线程堵塞,直到第二
    个线程执行完毕或者超过设置的等待毫秒数。
  • yield():暂停当前线程,把执行机会让给优先级相同或更高的线程执行,不会进入
    堵塞状态,直接强制切换为就绪状态,因此可能刚切换完yield方法,线程又获得了处理器
    资源,然后又继续执行了,该方法没有声明抛出任何异常。另外,如果没有优先级相同或者
    更高的线程,yield()方法什么都不做。

Object类中

疑问:为何这三个方法在Object类中?
答:每个对象都拥有一个monitor(锁),让当前线程等待某个对象的锁,应该通过这个对象操作,
而不是用当前线程来操作,因为当前线程可能等待的事多个线程的锁,用线程来操作,会非常复杂。

  • wait():让当前线程等待,直到通过notify()和notifyAll()或等待时间结束,当前
    线程进入等待队列时,当前的线程必须获得该对象的内部锁,因此调用wait()方法必须在同
    步块或同步方法中进行,如果该线程没获得对象锁的话,是会抛出IllegalMonitorStateException
    异常的,另外和sleep()不同,wait()方法会让出对象锁,然后进入等待状态;
  • notify():通知随机唤醒一个在等待队列中等待锁的线程,使该线程从堵塞状态
    切换到就绪状态;同样调用该方法的线程需要获得对象锁,所以也需要些在同步块/方法中进行。
  • notifyAll():唤醒等待队列中等待对象锁的所有线程,但是也是只有一个会拿到对象锁,
    至于是谁拿到锁就看系统的调度了。

几个不安全,不推荐的方法

  • stop():停止线程,可能导致ThreadDeath异常,导致程序崩溃。
  • interrupt():终端线程。
  • suspend()/resume():和wait,notify差不多,但容易引起死锁,不建议使用。

Java 1.5 新增Condition接口

用来代替wait(),notify(),notifyAll()实现实现线程间的协作,使用await(),signal(),
signalAll()来实现线程间的协作更加安全高效;Condition**依赖于Lock接口**,调用Lock对象的
newCondition()可以生成Condition,Condition的await(),signal()和signalAll()方法调用,
需要在lock()和unlock()之间使用


5.线程同步安全问题

当有两个或以上线程在同一时刻访问操作同一资源,可能会带来一些问题,比如:
数据库表中不允许插入重复数据,线程1,2都得到了数据X,然后线程1,2同时查询了
数据库,发现没有数据X,接着两线程都往数据库中插入X,然后就GG了,这就是线程
安全问题,而这里的数据库资源我们又称为:临界资源(共享资源)

6.如何解决线程安全问题

基本所有并发模式在解决线程安全问题时,都采用”系列化访问临界资源“的方式,
就是:同一时刻,只能有一个线程访问临界资源,也成”同步互斥访问“。通常就是
在操作临界资源的代码前加一个锁,当有线程访问资源,获取这个所,其他线程无法
访问,只能等待(堵塞),等这个线程访问完临界资源,然后释放锁,供其他线程继续访问。
而Java中提供两种方案来实现同步互斥访问:synchronizedLock,等下详细讲。

7.与锁相关的特殊情况:死锁,饥饿与活锁

每个对象都拥有一个锁,当多个线程,操作涉及到多个锁,就可能会出现这三种情况:

死锁(DeadLock)

两个或以上进程(线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,
如果无外力作用,他们将继续这样僵持下去;简单点说:两个人互相持有对方想要的资源,
然后每一方都不愿意放弃自己手上的资源,就一直那样僵持着。

死锁发生的条件

互斥条件(临界资源);
请求和保持条件(请求资源但不释放自己暂用的资源);
不剥夺条件(线程获得的资源只有线程使用完后自己释放,不能被其他线程剥夺);
环路等待条件:在死锁发生时,必然存在一个”进程-资源环形链”,t1等t2,t2等t1;

如何避免死锁

破坏四个条件中的一个或多个条件,常见的预防方法有如下两种:
有序资源分配法:资源按某种规则统一编号,申请时必须按照升序申请:
1.属于同一类的资源要一次申请完;2.申请不同类资源按照一定的顺序申请。
银行家算法:就是检查申请者对资源的最大需求量,如果当前各类资源都可以满足的
申请者的请求,就满足申请者的请求,这样申请者就可很快完成其计算,然后释放它占用
的资源,从而保证了系统中的所有进程都能完成,所以可避免死锁的发生。
理论上能够非常有效的避免死锁,但从某种意义上说,缺乏使用价值,因为很少有进程
能够知道所需资源的最大值,而且进程数目也不是固定的,往往是不断变化的,
况且原本可用的资源也可能突然间变得不可用(比如打印机损坏)。

饥饿(starvation)与饿死(starve to death)

资源分配策略有可能是不公平的,即不能保证等待时间上界的存在,即使没有发生死锁,
某些进程可能因长时间的等待,对进程推进与相应带来明显影响,此时的进程就是
发生了进程饥饿(starvation),当饥饿达到一定程序即此时进程即使完成了任务也
没有实际意义时,此时称该进程被饿死(starve to death),典型的例子:
文件打印,采用短文件优先策略,如果短文件太多,长文件会一直推迟,那还打印个毛。

活锁(LiveLock)

特殊的饥饿,一系列进程轮询等待某个不可能为真的条件为真,此时进程不会进入blocked状态,
但会占用CPU资源,,活锁还有几率能自己解开,而死锁则无法自己解开。(例子:都觉得对方
优先级比自己搞,相互谦让,导致无法使用某资源),简单避免死锁的方法:先来先服务策略


8.守护线程

也叫后台线程,是一种为其他线程提供服务的一种线程;当虚拟机检测到没有用户进程可服务的
时候,就会退出当前应用程序的运行,比如JVM的gc(垃圾回收线程),当我们程序中不再有任何
Thread的时候,垃圾回收就没事做了,即使还有其他后台线程,但是此时的JVM还是会退出!
将一个线程设置为后台线程:setDaemon(boolean)
判断一个线程是否为后台线程:isDaemon()


9.线程并发的问题

开始的问题:临时数据存放在内存中,而CPU执行指令比内存读写快太多!
解决方法:CPU中使用高速缓存,将需要数据从内存复制一份到缓存中,运算时CPU直接读缓存,
运算结束后,再将缓存中的数据刷新回内存中。

又有问题:单线程倒没什么,多线程的话,切线程处于不同的CPU中,每个线程都拥有自己的
高速缓存,这样可能会出现缓存不一致的问题,比如内存中的i = 0,然后在两个线程中都执行
i = i + 1;分别在两个线程中执行,最后的结果可能是1,而不是2。
硬件层次的解决方案在总线加LOCK#锁的方式缓存一致性协议(Intel的MESI协议)

10.并发编程的三个概念

  • 原子性:一个操作或多个操作,要么全部执行,并且执行过程不会被任何因素打断;
    要么就都不执行,比如:银行转账的栗子。
  • 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够
    立即看到修改后的值。
  • 有序性:程序执行的顺序按照代码的先后顺序执行。可能你觉得自己程序怎么写的执行
    顺序就是怎样,只能说naive!有个名词叫:指令重排序!一般来说处理器为了提高程序
    的运行效率,可能会对输入的代码进行优化,它不保证程序中每个语句的执行先后顺序一致
    但是他会保证程序最终执行结果和代码顺序执行的结果是一致的!另外处理器在重排序的
    时候还会考虑指令的数据依赖性,如果指令2需要用到指令1的结果,那么处理器会保证
    指令1会在指令2之前执行,指令重排序不会音箱单线程的执行,但是在多线程并发的时候可能
    会影响结果的正确性。

综上:想要保证并发程序能够正确执行,必须保持原子性,可见性,有序性,只有有一个没
保证,就有可能会导致运行不正确。

11.Java中对并发编程的保证与8条先行发生原则

Java内存模型规定所有变量都是存储在主存中,每个线程都有自己的工作内存(类似于前面的高速
缓存),线程对变量的所有操作必须在工作内存中,而不能直接对主存进行操作,且每个线程不能
访问其他线程的工作内存。

Java中语言本身对原子性,可见性,有序性提供了哪些保证?

  • 原子性:Java内存模型只保证了基本读取和赋值原子性操作,如果要实现
    更大范围操作的原子性,可以通过synchronized和Lock来实现。
  • 可见性:Java提供了volatile关键字保证可见性,当一个共享变量被
    volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,
    他会去内存中读取新值。另外,通过synchronized和Lock也能够保证可见性。
  • 有序性:Java内存模型中,允许编译器和处理器对指令进行重排序,以通过volatile
    关键字来保证一定的”有序性”,通过synchronized和Lock也能够保证有序性。
    另外,Java内存模型具有一些先天的”有序性”,即不需要通过任何手段就能够得到保证的有序性
    这个通常也称为happens-before原则,如果两个操作的执行次序无法从happens-before原则
    推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

8条happens-before原则(先行发生原则)

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
    该规则用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作。
    就是同一个锁如果处于被锁定状态,那么必须先对锁进行释放操作,后面才能继续进行lock操作。
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
    直观解释:若一个线程先去写一个变量,然后一个线程去读取,那么写入操作肯定先与读操作执行。
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先
    行发生于操作C。说明happens-before原则具备传递性。
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()
    方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

12.线程并发的经典问题:生产者消费者问题

问题概述

两个共享固定缓冲区大小的线程,生产者线程负责生产一定量的数据放入缓冲区,
而消费者线程则负责消耗缓冲区中的数据,关键问题是需要保证
1.缓冲区满的时候,生产者不再往缓冲区中填充数据
2.缓存区空的时候,消费者不在消耗缓冲区中的数据

可以用简单的两种套路来模拟这个问题:
synchronized + wait() + notify()方式 或 Lock + Condition接口的await()与signal()实现
等下细讲。


13.同步容器

Java集合容器中有四类:List(数组)Set(集合)Queue(队列)Map,前三个都继承Collection接口,
Map本身是一个接口。我们平常使用的ArrayList,LinkedList,HashMap这些容器都是非线程安全的,并发访问的时候可能会
有问题,然后Java给我们提供了两类的同步容器:

  • VectorStackHashTable(普通集合加了同步措施而已)
  • Collections类中提供的静态工厂方法创建的类(不是Connection类!!!提供了静态工厂方法来创建同步容器,还提供
    了对集合或容器进行排序,查找等的操作。)

技术分享

相比起非同步容器,同步容器因为锁的关系,性能会稍弱。
另外同步容器也不一定是安全的,只能保证每个时刻只能有一个线程在访问他,集合删元素遍历的例子,
有时为了保证线程安全,还需要在方法调用端做额外的同步措施!

另外,在对Vector等容器并发地进行迭代修改时,会报ConcurrentModificationException异常!
原因是:Iterator执行next方法时,调用checkForComodification()时expectedModCount与modCount不相等,
由于执行remove(),modCount每次循环值都会改变,而expectedModCount并没改变,所以抛异常!

解决方法

  • 1.使用Iterator提供的remove方法来删除当前元素。
  • 2.另外创建一个集合来存放要删除的元素,然后调用removeAll统一删除。
  • 3.自己通过索引来删除,要保证索引是正常的,比如删除了4,你的索引要变回3;
    另外这些可能不是线程安全的,想保证多线程执行也安全的话,可以:
  • 1.使用Iterator迭代的时候,加同步锁
  • 2.使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。

14.并发容器

同步容器因为使用Synchronized进行同步,执行读写都需要去获取锁,并发的时候效率较低,
Jdk 1.5新增的concurrent提供了这些并发容器:

  • BlockingQueue接口:线程安全的堵塞式队列,线程安全的阻塞式队列;当队列已满时,向队列
    添加会阻塞;当队列空时,取数据会阻塞。(非常适合消费者-生产者模式)
    阻塞方式:put()、take()。
    非阻塞方式:offer()、poll()。
    实现类:基于数组的固定元素个数的ArrayBolockingQueue和基于链表结构的不固定元素个
    数的LinkedBlockQueue类。
  • BlockingDeque接口: 与BlockingQueue相似,但可以对头尾进行添加和删除操作的双向队列;
    方法分为两类,分别在队首和对尾进行操作。
    实现类:标准库值提供了一个基于链表的实现,LinkedBlockgingDeque
  • ConcurrentMap接口: 继承自java.util.Map接口
    putIfAbsent():只有在散列表不包含给定键时,才会把给定的值放入。
    remove():删除条目。
    replace(key,value):把value 替换到给定的key上。
    replace(key, oldvalue, newvalue):CAS的实现。
    实现类:ConcurrentHashMap
    创建时,如果可以预估可能包含的条目个数,可以优化性能。(因为动态调整所能包含的数目操作比较耗时
    这个HashMap也一样,只是多线程下更耗时)。创建时,预估进行更新操作的线程数,这样实现中会根据这
    个数把内部空间划分为对应数量的部分。(默认是16,如果只有一个线程进行写操作,其他都是读取,
    那么把值设为1 可以提高性能)。
    注:当从集合中创建出迭代器遍历Map元素时,不一定能看到正在添加的数据,只能和集合保证弱一致性。
    (当然使用迭代器不会因为查看正在改变的Map,而抛出java.util.ConcurrentModifycationException)
  • CopyOnWriteArrayList/CopyOnWriteArraySet:当往容器中添加数据时,不是直接添加,而是将当前容器进行Copy
    然后往新容器里添加,添加完后,再把旧容器的引用只想新容器,读写分离的思想。可以参考CopyOnWriteArrayList写个
    CopyOnWriteMap,套路:在put和putAll的时候加synchronized锁,创建新集合放值,然后旧的指向新的集合就好。
    注意:创建时初始化好大小,避免扩容开销;批量添加,减少容器的复制次数;只保证数据的最终一致性,不保证
    数据的实时一致性!

15.阻塞队列

在解决生产者与消费者问题时,我们的套路一般是对容器加锁,然后判断容器中数据满和空的情况,
然后唤醒或者阻塞生产者或者消费者线程,有些麻烦,如果使用阻塞队列,我们就不用关心那么多,
阻塞队列会对访问线程产生堵塞,比如当地队列满了,此时生产者线程会被阻塞,直到消费者消费
了队列中的元素,被堵塞的线程会自动唤醒。其实就是把wait(),notify()这些集成到队列中实现。

几种主要的阻塞队列

同样是java.util.concurrent包下提供的若干个阻塞队列:

  • ArrayBlockingQueue:基于数组实现的,创建时需指定容量大小,也可以指定公平性与非公平性,
    默认非公平,即不保证等待时间最长的队列最优先能够访问队列。
  • LinkedBlockingQueue:基于链表实现的,创建时不指定容量大小的话,默认大小为Integer.MAX_VALUE。
    PriorityBlockingQueue:前面两个都是先进先出队列,而PriorityBlockingQueue是按照元素优先级对
    元素进行排序,按照优先级顺序出队,即每次出队的都是优先级最高的元素;另外,该队列是无界阻塞队列,
    即容量没有上线,而前两种是有界队列。
    DelayQueue:基于PriorityQueue,延时阻塞队列,队列中的元素只有当其延时时间到了,才能够从队列中获取到
    该元素,同样是无界队列,因此往队列里插入元素(生产者)永远不会被阻塞,而只有获取数据(消费者)才
    会被阻塞。

堵塞队列除了对非阻塞队列的下述五个方法进行了同步:

  • add(E e):添加元素到队尾,插入成功返回true,若插入失败(队满),抛出异常。
  • remove():移除队首元素,移除成功返回true,若移除失败(队空),抛出异常。
  • offer(E e):添加元素到队尾,插入成功返回true,若插入失败(队满),返回false。
  • poll():移除并获取队首元素,成功返回元素,否则返回null。
  • peek():获取队首元素,成功返回队首元素,否则返回null。

还提供了另外4个非常有用的方法:

  • put(E e):向队尾存入元素,队满则等待。
  • take():取队首元素,队空则等待。
  • offer(E e,long timeout,TimeUnit unit):往队尾存元素,队满等待一定时间,时间到后,若没有插入成功,
    返回false,否则返回true;
  • poll(long timeout,TimeUnit unit):取队首元素,队空等待一定时间,时间到后,没取到返回null,否则
    返回取得的元素。

阻塞队列适用于生产者-消费者问题,因为不需要再单独考虑同步和线程间通信的问题。


16.线程组

我们可以通过java.lang.ThreadGroup对线程进行组操作,每个线程都归属于某个线程组管理的一员。
在创建Thread实例时,如果没有制定线程组参数,则默认属于创建者线程所隶属的线程组,这种隶属
关系在创建新线程时指定,在线程的整个生命周期里都不能改变!比如我们在main()中创建的新线程,
这个线程属于main这个线程管理中的一员!

作用

简化对多个线程的管理,对若干线程同时操作,比如:调用线程组的方法设置所有线程的优先级,
调用线程组的犯法启动或堵塞组中所有线程等;其实,线程组的最重要意义是线程安全,Java默认
创建的线程都是属于系统线程组,而处于同一线程组中的线程可以互相修改对方数据,当如果在不
同的线程组中,那么就不能”跨线程组”修改数据,从一定程度上保证了数据的安全。

线程组提供的操作

  • 集合管理方法:用于管理包含在线程组中的线程与子线程组
  • 组操作方法:设置或获取线程对象的属性
  • 组中所有偶线程的操作方法,针对组中所有线程与子线程执行某一操作,比如:线程启动,恢复
  • 访问限制方法:基于线程组的成员关系

常见用法

  • 获得当前线程的线程组对象:ThreadGroup tGroup = Thread.currentThread().getThreadGroup();
  • 获得线程组的名字:tGroup.getName();
  • 获得线程中活动线程的数目:tGroup.activeCount();
  • 将线程组中每个活动线程复制到线程数组中:tGroup.enumerate(Thread list[])
  • 获得本线程组的父线程组:tGroup.getParent();
  • 判断某个线程组是否为守护线程组:tGroup.isDaemon();

使用示例

public class ThreadGroupTest {
    public static void main(String[] args) {
        ThreadGroup tGroup = new ThreadGroup("自定义线程组");
        Thread t1 = new Thread(tGroup,"自定义线程1");
        Thread t2 = new Thread(tGroup,"自定义线程2");
        System.out.println("线程组的初始最大优先级:" + tGroup.getMaxPriority());
        System.out.println(t1.getName() + "的初始优先级:" + t1.getPriority());
        System.out.println(t2.getName() + "的初始优先级:" + t2.getPriority());
        t1.setPriority(9);  //设置t1的优先级为9
        tGroup.setMaxPriority(8);   //设置线程组的优先级为8
        System.out.println("线程组的新最大优先级:" + tGroup.getMaxPriority());
        System.out.println(t1.getName() + "的新优先级" + t1.getPriority());
        t2.setPriority(10); //设置t2的优先级为10
        System.out.println(t2.getName() + "的新优先级" + t2.getPriority());
        System.out.println(tGroup.toString());
    }
}

运行结果

技术分享


17.线程池

引入

直接创建的线程在调用完start()方法结束后,线程就结束了,而线程的创建与结束都需要耗费
一定的系统时间,如果并发的线程数量很多的话,不停的创建和销毁线程会消耗大量的时间,
效率就有些低了,我们想让线程在执行完任务以后并不销毁,而是让他进入休眠状态,然后
继续执行其他任务,当需要用到这个线程再唤醒,Java中使用线程池可以达到这样的效果。

ThreadPoolExecutor类

继承AbstractExecutorService类,并提供四种构造方法(前三其实都是调用第四个进行初始化
工作的):

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { }

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { }

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { }

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { }

参数解析

  • corePoolSize:核心池大小,创建线程池后,默认池中没有任何线程,而是等待有任务来才去
    创建线程执行任务,除非调用prestartAllCoreThreads()或prestartCoreThread()方法预创建线
    程,就是在没有任务来之前就创建corePollSize个线程或者一个线程,当池中线程数目达到
    corePoolSize后,会把后到达的任务放到缓存队列中。
  • maximumPoolSize:线程池最大线程数,表示线程池中最多创建多少个线程。
  • keepAliveTime:线程没有任务执行最多保持多久时间会终止,只有当池中线程数大于
    corePoolSize才会起作用,直到池中线程数不超过corePoolSize。但是如果调用了
  • allowCoreThreadTimeOut(boolean)方法,即使池中线程不大于CorePoolsize,
    该参数也会起作用,知道池中线程数为0。
  • unit:keepAliveTime的时间单位,比如毫秒:TimeUnit.MILLISECONDS 等。
  • workQueue:堵塞队列,用来存储等待执行的任务,一般来说有以下几种选择:
    ArrayBlockingQueue(基于数组的并发堵塞队列)
    LinkedBlockingQueue(基于链表的FIFO堵塞队列)
    PriorityBlockingQueue(带优先级的无界堵塞队列)
    SynchronousQueue(并发同步堵塞队列)
  • threadFactory:线程工厂,用于创建线程。
  • handler:表示拒绝处理任务时的策略,有四种取值:
    ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
    ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
    ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

线程池相关的几个类之间的关系

技术分享

使用示例

MyThread.java

public class MyThread implements Runnable {
    private int threadNum;

    public MyThread(int threadNum) {
        this.threadNum = threadNum;
    }

    @Override
    public void run() {
        System.out.println("线程:" + threadNum + "开始执行...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程:" + threadNum + "执行完毕...");
    }
}

ThreadTest.java 测试类:

public class ThreadTest {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200,
                TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(5));
        for (int i = 0; i < 15; i++) {
            MyThread myThread = new MyThread(i);
            executor.execute(myThread);
            System.out.println("线程池中线程数目:" + executor.getPoolSize() + " 等待执行的任务数目:" + executor.getQueue().size());
        }
        executor.shutdown();
    }
}

另外,Java文档中并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors提供的几个静态方法来创建线程池:

  • Executors.newSingleThreadExecutor():创建容量为1的线程池,可以理解为串行执行所有任务,任务执行顺序与提交顺序相同。
  • Executors.newFixedThreadPool():创建固定容量的线程池
  • Executors.newCachedThreadPool():创建可以缓存的线程池,缓冲池容量大小为Inter.MAX_VALUE,可以灵活的往池中添加线程,
    如果线程空闲超过了指定时间,该工作线程会终止,终止后如果你提交了任务,线程池会重新创建一个工作线程。
  • Executors.newScheduledThreadPool():创建不限容量的线程池,支持定时及周期性执行任务的需求,多了个schedule(thread,time)
    延时执行的方法。

当然,如果这个几个线程池都满足不了你的话,你可以继承ThreadPoolExecutor类重写。


18.Timer和TimerTask

Timer是Jdk提供的定时器类,延时或者重复执行任务,使用时候会主线程外开启
单独的线程来执行定时任务,可以指定执行一次或重复多次。TimerTask是实现了
Runnable接口的抽象类,代表一个可以被Timer执行的任务。

Timer timer = new Timer();
timer.schedule(new TimerTask() {
    public void run() { ... timer.cancle(); }
},延时时间)

终止Timer的几个方式

  • 1.调用timer的cancle();
  • 2.将timer线程设置为守护线程;
  • 3.所有任务执行完后,删除timer对象的引用,线程也会被终止;
  • 4.System.exit()终止程序;

另外,schedule保证每次延迟间隔固定,而scheduleAtFixedRate则可能因为某个调度
时间太长,而缩短间隔的方式,保证下一次调度在预定时间执行,比如,每隔3s调度一次:
正常都是:0,3,6,9,假如第二次调度花了2s,前者会变成:0,3+2,8,11,而后者会压缩间隔,
保证调度时间,变成:0,3+2,6,9,另外还要注意Timer不保证任务执行的十分准确!


19.三个并发辅助类:CountDownLatch,CyclicBarrier和Semaphore

CountDownLatch(类似于计时器,比如有任务A,需等其他4个任务执行完毕才执行,就可以用上)
使用方法如下:

CountDownLatch latch = new CountDownLatch[2];  //初始值
latch.await();  //调用了这个方法的那个线程会被挂起,直到count = 0才继续执行,也可以设置时间,
                //超时count还没变0就会继续执行
latch.countDown();  //count值减1

CyclicBarrier(回环栅栏,让一组线程等待到某个状态再全部同时执行,所有等待线程被释放后,CyclicBarrier可以重用)
比如:若干个线程需要进行写操作,并且想所有线程都达到某个状态才能执行后续任务,此时可以用CyclicBarrier。
使用方法如下:

CyclicBarrier barrier = new CyclicBarrier(parties); //参数是指定多个线程达到某状态
                                                    //如果你想执行完任务后,做其他操作可加个Runnable的参数。
barrier.await();    //挂起当前线程,直到到达某个状态再同时执行,同样可以设置时间,超时直接
                    //执行已经到达这个状态的线程

PS:个人简单理解:调了await()会挂起这个线程,然后+1,直到结果等于parties,再继续执行挂起线程的后续部分。

Semaphore(信号量,控制某资源同时被几个线程访问的类,与锁类似)
使用方法如下:

Semaphore semaphore = new Semaphore(5); //设置多少个线程同时访问,可选参数boolean fair表示
                                        //是否公平,即等待越久越先获得许可
semaphore.acquire();    //获取一个许可
semaphore.acquire(int);    //获取多个许可
semaphore.release() //释放一个许可
semaphore.release(int) //释放多个许可

//acquire获取许可的方式会堵塞,就是没有拿到的话会一直等待,如果想立即得到结果
可调用:tryAcquire()

semaphore.availablePermits()    //获得可用许可数目

20.ThreadLocal(线程本地存储)

作用:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,
减少同一个线程内多个函数或者组件之间一些公共变量传递的复杂度同时隔离其他线程
你可以:

  • 重写initialValue()方法来设置ThreadLocal的初始值
  • get():获得当前线程的ThreadLocal的值
  • set():设置当前线程的ThreadLocal的值
  • remove():删除当前线程的ThreadLocal绑定的值,某些情况下需要手动调用该函数,防止内存泄露。

用法示例

public class ThreadTest {
    private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for (int i =0;i < 5;i++) {
            ThreadTest test = new ThreadTest();
            new Thread(test.new MyThread(i)).start();
        }
    }

    class MyThread implements Runnable {
        private int index;

        public MyThread(int index) {
            this.index = index;
        }

        @Override
        public void run() {
            System.out.println("线程" + index + "的初始value:" + value.get());
            for (int i = 0; i < 10; i++) {
                value.set(value.get() + i);
            }
            System.out.println("线程" + index + "的累加value:" + value.get());
            try {
                Thread.sleep(1000l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果

技术分享

ThreadLocal的实现原理

每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,
value是真正需要存储的Object。

更多详细内容可见:[Java并发包学习七]解密ThreadLocal


细讲与代码实现


1.synchronized同步方法或代码块

在Java中每个对象都拥有一个monitor(互斥锁标记),也称为监视器,当多个线程访问某
个对象时,线程只有获得该对象的锁才能访问。使用synchronized关键字来获取对象上的锁,
可应用在方法级别(粗粒度锁)代码块级别(细粒度锁)

同步方法

比如: public synchronized void save(){}

同步代码块

比如:synchronized(资源对象){ }

类锁

每个类都有一个类锁,用于类的静态方法或者一个类的class对象上的(单例),类的对象实例
可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰
的,但是每个类只有一个类锁。类锁与对象锁是两种不同的锁,控制着不同区域,互不干扰,
统一,线程获得对象锁的同时,也可以获取类锁,同时获得两个锁是允许的。
用法比如:public static synchronized insert() {}; synchronized(类.class)

注意事项

  • 1.当有线程在访问对象的synchronized方法,其他线程不能访问该对象的其他
    synchronized方法!但可以访问非synchronized方法。
  • 2.对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动
    释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。
  • 3.一个线程可以获得多个锁标记,一个对象最多只能把锁标记给一个线程
    synchronized是以牺牲程序效率为代价的,因此应该尽量控制互斥代码块的范围。
  • 4.方法的Synchronized特性本身不会被继承,只能覆盖。
  • 5.线程因未拿到锁标记而发生的堵塞不同于基本状态中的堵塞,等待的线程
    会进入该对象的锁池(放置等待获取锁标记的线程)中,锁池中哪个线程拿到锁
    标记由系统决定。

2.Lock(锁)

Synchronized的缺陷

使用Synchronized获取锁的线程释放锁的两种情况

  • 1.获取锁的线程执行完该代码块,然后线程释放对锁的占有;
  • 2.线程执行发生异常,此时JVM会让线程自动释放锁;

如果是IO等待或其他原因(调sleep方法)被堵塞了,但又没释放锁,只能一直等待;
另外当多个线程读写文件时,读和写会冲突,写和写会冲突,但是读与读不该冲突。
而使用Lock可以解决上述问题,而且Lock还可以知道有没有获得锁,Lock类可以实
现同步访问,另外synchronized不需要用户自己手动去释放锁,而Lock需由用户去
手动释放锁,若果没有主动释放的话,就有可能导致出现死锁现象。

Lock源码解析

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}
  • lock():获取锁,如果锁已被其他线程获取,则进行等待
  • unlock():释放锁,Lock必须主动去释放锁,所以Lock必须在try-catch中进行,在finally中释放。
  • tryLock():尝试获取锁,获取成功返回true,获取失败返回false,无论如何会立即返回,而不会
    在因拿不到锁就一直在那里等待;如果是有参的那个,拿不到锁会等待一段时间,时间到了也会立即返回。
  • lockInterruptibly():当通过该方法来获取锁时,如果已经有某个进程持有了这个锁,而另一线
    程需要等待,那么对另一个线程调用interrupt方法中断等待过程。

ReentrantLock(可重入锁,独占锁):唯一实现Lock接口的类

示例代码

public class Main {
    private ArrayList<Integer> arrayList = new ArrayList<>();
    private Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        final Main main = new Main();
        for(int i = 0;i < 2;i++) {
            new Thread(){
                @Override
                public void run() {
                    super.run();
                    main.lock(Thread.currentThread());
                }
            }.start();
        }
    }

    /**lock()*/
    public void lock(Thread thread) {
        lock.lock();
        duplicated(thread);
    }

    /**tryLock()*/
    public void tryLock(Thread thread) {
        if(lock.tryLock()) {
            duplicated(thread);
        } else {
            System.out.println(thread.getName()+"获取锁失败");
        }
    }

    //相同代码
    public void duplicated(Thread thread) {
        try {
            System.out.println(thread.getName() + "得到了锁");
            for(int i=0;i<5;i++) { arrayList.add(i); }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }
}

运行结果技术分享 技术分享

ReadWriteLock接口

读写操作分开成两个锁,一个资源能够被多个读线程访问,或者被一个写线程访问,
但是不能同时存在读写线程,使用场合:共享资源被大量读取操作,只有少量写操作(修改数据)

ReentrantReadWriteLock(读写锁):

readLock()和writeLock()用来获取读锁和写锁,读锁是共享锁,能同时被多个线程获取;
写入锁是独占锁,只能被一个线程锁获取。


3.锁的相关概念

  • 1.可重入锁:synchronized和ReentrantLock都是可重锁,比如线程执行某个synchronized方法
    在这个方法里会调该类中的另一个synchronized方法,此时不用重复申请锁,可以直接执行该方法。
  • 2.可中断锁:可以中断的锁,在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
  • 3.公平锁:尽量以请求锁的顺序来获取锁,当这个所释放时,等待时间最久的线程(最先请求)
    会获得该锁,这就是公平锁,非公平锁无法保证按顺序,就可能导致某个/一些线程永远获取不到锁。
    synchronized就是非公平锁,而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非
    公平锁,但是可以设置为公平锁,构建的时候传参(true表示公平锁,false为非公平锁,用无参构
    造方法,则是非公平锁),另外记住一点:ReentrantReadWriteLock并未实现Lock接口,它实现的是
    ReadWriteLock接口。
  • 4.读写锁:将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁,
    ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
    可以通过readLock()获取读锁,通过writeLock()获取写锁。

4.生产者与消费者的几种代码实现

synchronized + wait() + notify()方式

实现核心:定义一个仓库类,对于生产和消耗方法加synchronized锁;
定义两个线程,生产者和消费者,对于满或空的情况进行判断,wait()和notify();

产品类:Product.java

public class Product {
    private int productId = 0;
    public Product() {  }
    public Product(int productId) {
        this.productId = productId;
    }
    public int getProductId() {
        return productId;
    }
    public void setProductId(int productId) {
        this.productId = productId;
    }
    @Override
    public String toString() {
        return "Product{" +
                "productId=" + productId +
                ‘}‘;
    }
}

仓库类:WareHouse.java

public class WareHouse {
    private int base = 0;
    private int top = 0;
    private Product[] products = new Product[10];
    public synchronized void produce(Product product) {
        notify();
        while (top == products.length) {
            try {
                System.out.println("仓库已满,暂停生产,等待消费者消费...");
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        products[top] = product;
        top ++;
    }
    public synchronized Product consume() {
        Product product = null;
        while (top == base) {
            notify();
            try {
                System.out.println("仓库已空,暂停消费,等待生产者生产...");
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        top--;
        product = products[top];
        products[top] = null;
        return product;
    }
}

生产者线程:Producer.java:

public class Producer implements Runnable {
    private String produceName;
    private WareHouse wareHouse;
    public Producer() { }
    public Producer(String produceName, WareHouse wareHouse) {
        this.produceName = produceName;
        this.wareHouse = wareHouse;
    }
    public String getProduceName() {
        return produceName;
    }
    public void setProduceName(String produceName) {
        this.produceName = produceName;
    }
    @Override
    public void run() {
        int i = 0;
        int j = 0;
        while (j < 100) {
            i++;
            j++;
            Product product = new Product(i);
            wareHouse.produce(product);
            System.out.println(getProduceName() + "生产了" + product);
            try {
                Thread.sleep(200l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

消费者线程:Consumer.java

public class Consumer implements Runnable{
    private String consumerName = null;
    private WareHouse wareHouse = null;
    public Consumer() { }
    public Consumer(String consumerName, WareHouse wareHouse) {
        this.consumerName = consumerName;
        this.wareHouse = wareHouse;
    }
    public String getConsumerName() {
        return consumerName;
    }
    public void setConsumerName(String consumerName) {
        this.consumerName = consumerName;
    }
    @Override
    public void run() {
        int j = 0;
        while (j < 100) {
            j++;
            System.out.println(getConsumerName() + "消费了" + wareHouse.consume());
            try {
                Thread.sleep(300l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

测试类:Test.java

public class Test {
    public static void main(String[] args) {
        WareHouse wareHouse = new WareHouse();
        Producer producer = new Producer("生产者",wareHouse);
        Consumer consumer = new Consumer("消费者",wareHouse);
        Thread t1 = new Thread(producer);
        Thread t2 = new Thread(consumer);
        t1.start();
        t2.start();
    }
}

Lock + Condition接口的await()与signal()实现

和上面代码没太大区别,只是改了下仓库类:ConditionWareHouse.java:
这里可以只用一个Condition,把signal改成signalAll()即可。

public class ConditionWareHouse {
    private LinkedList<Product> products = new LinkedList<>();
    private static final int MAX_SIZE = 10; //仓库容量
    private Lock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();
    public  void produce(Product product) {
        lock.lock();
        try {
            while (products.size() == MAX_SIZE) {
                System.out.println("仓库已满,暂停生产,等待消费者消费...");
                notFull.await();
            }
            products.add(product);
            notEmpty.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public  Product consume() {
        lock.lock();
        Product product = null;
        try {
            while (products.size() == 0) {
                System.out.println("仓库已空,暂停消费,等待生产者生产...");
                notEmpty.await();
            }
            product = products.removeLast();
            notFull.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return product;
    }
}

使用堵塞队列实现

public class ThreadTest {

    private static final int MAX_SIZE = 10; //仓库容量
    private  ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(MAX_SIZE);

    public static void main(String[] args) {
        ThreadTest test = new ThreadTest();
        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();
        new Thread(producer).start();
        new Thread(consumer).start();
    }

    class Producer implements Runnable {
        @Override
        public void run() {
            for(int i =0;i < 100;i++) {
                try {
                    queue.put(i);
                    System.out.println("往仓库中放入元素(" + i + ")剩余容量为:" + (MAX_SIZE - queue.size()));
                    Thread.sleep(50l);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    class Consumer implements Runnable {
        @Override
        public void run() {
            for(int i =0;i < 100;i++) {
                try {
                    queue.take();
                    System.out.println("从仓库中取走元素(" + i + ")剩下元素:" + queue.size());
                    Thread.sleep(100l);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

5.volatile关键字详解

深入剖析volatile关键字

  • volatile关键字的两层语义可见性禁止进行指令重排序
    使用volatile修饰的变量,会强制将修改的值立即写入主存,当写入的时候,
    会导致工作内存缓存的变量的缓存行无效,线程会再去主存中读取变量。
  • volatile保证原子性吗
    volatile无法保证对变量的任何操作都是原子性的,比如自增操作不是原子性的,
    可以通过synchronized和Lock来实现,另外jdk 1.5在java.util.concurrent.atomic包下
    提供了一些原子操作类,比如自增AtomicInteger
  • volatile能保证有序性吗?volatile关键字禁止指令重排序有两层意思:
    1.当程序执行到volatile变量的读写操作,在其前面的操作的更改肯定全部已经进行,
    且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
    2.在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,
    也不能把volatile变量后面的语句放到其前面执行。

volatile的原理与实现机制

 
摘自:《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键
字时,会多出一个lock前缀指令” lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),
内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面
的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

volatile关键字的使用场景

状态量标记

//状态量标记
volatile boolean flag = false;
while(!flag){
    doSomething();
}
public void setFlag() {
    flag = true;
}
//多线程
volatile boolean inited = false;
//线程1:
context = loadContext();  
inited = true;            
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

双重校验锁

class Singleton{
    private volatile static Singleton instance = null;
    private Singleton() { }
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

MusicTime技术分享 :静心~大悲咒

<iframe border="0" width="330" height="86" src="http://music.163.com/outchain/player?type=2&id=344719&auto=1&height=66" __idm_id__="425985"></iframe>

本文内容部分摘自

  • 1.海子-Java并发编程
  • 2.并发编程一波流之CyclicBarrier
  • 3.[Java并发包学习七]解密ThreadLocal
  • 4.Timer和TimerTask详解
  • 5.Java 多线程 并发编程
<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Java-Review-Note——4.多线程