一,基础概念
1.1什么是CPU
中央处理器(CPU),是电子计算机的主要设备之一,电脑中的核心配件。其功能主要是解释计算机指令以及处理计算机软件中的数据。CPU是计算机中负责读取指令,对指令译码并执行指令的核心部件。中央处理器主要包括两个部分,即控制器、运算器,其中还包括高速缓冲存储器及实现它们之间联系的数据、控制的总线。电子计算机三大核心部件就是CPU、内部存储器、输入/输出设备。中央处理器的功效主要为处理指令、执行操作、控制时间、处理数据。
1.2CPU核心数和线程数的关系
多核心:也指单芯片多处理器( Chip Multiprocessors,简称CMP),CMP是由美国斯坦福大学提出的,其思想是将大规模并行处理器中的SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个CPU同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理
多线程: Simultaneous Multithreading.简称SMT.SMT可通过复制处理器上的结构状态,让同一个处理器上的多个线程同步执行并共享处理器的执行资源可最大限度地实现宽发射、乱序的超标量处理,提高处理器运算部件的利用率,缓和由于数据相关或 Cache未命中带来的访问内存延时。
核心数、线程数:目前主流CPU有双核、三核和四核,六核也在2010年发布。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是1:1对应关系,也就是说四核CPU一般拥有四个线程。但 Intel引入超线程技术后,使核心数与线程数形成1:2的关系
1.3CPU时间片轮转机制
我们平时在开发的时候,感觉并没有受cpu核心数的限制,想启动线程就启动线程,哪怕是在单核CPU上,为什么?这是因为操作系统提供了一种CPU时间片轮转机制。
时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
我们平时在开发的时候,感觉并没有受cpu核心数的限制,想启动线程就启动线程,哪怕是在单核CPU上,为什么?这是因为操作系统提供了一种CPU时间片轮转机制。
时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾
时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一个进程是需要一定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切换( processwitch),有时称为上下文切换( context switch),需要5ms,再假设时间片设为20ms,则在做完20ms有用的工作之后,CPU将花费5ms来进行进程切换。CPU时间的20%被浪费在了管理开销上了。
为了提高CPU效率,我们可以将时间片设为5000ms。这时浪费的时间只有0.1%。但考虑到在一个分时系统中,如果有10个交互用户几乎同时按下回车键,将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的进程不得不等待5s才获得运行机会。多数用户无法忍受一条简短命令要5才能做出响应,同样的问题在一台支持多道程序的个人计算机上也会发生
结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了CPU效率:而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100ms通常是一个比较合理的折衷。
1.4什么是进程和线程
进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的、静态的,进程是活的、动态的。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。
线程是CPU调度的最小单位,必须依赖于进程而存在
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
1.5并行和并发
并发:指应用能够交替执行不同的任务,比如单CPU核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已.
并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行
两者区别:一个是交替执行,一个是同时执行.
1.6高并发编程的意义
由于多核多线程的CPU的诞生,多线程、高并发的编程越来越受重视和关注。多线程可以给程序带来如下好处。
(1)充分利用CPU的资源
从上面的CPU的介绍,可以看的出来,现在市面上没有CPU的内核不使用多线程并发机制的,特别是服务器还不止一个CPU,如果还是使用单线程的技术做思路,明显就out了。因为程序的基本调度单元是线程,并且一个线程也只能在一个CPU的一个核的一个线程跑,如果你是个i3的CPU的话,最差也是双核心4线程的运算能力:如果是一个线程的程序的话,那是要浪费3/4的CPU性能:如果设计一个多线程的程序的话,那它就可以同时在多个CPU的多个核的多个线程上跑,可以充分地利用CPU,减少CPU的空闲时间,发挥它的运算能力,提高并发量。
就像我们平时坐地铁一样,很多人坐长线地铁的时候都在认真看书,而不是为了坐地铁而坐地铁,到家了再去看书,这样你的时间就相当于有了两倍。这就是为什么有些人时间很充裕,而有些人老是说没时间的一个原因,工作也是这样,有的时候可以并发地去做几件事情,充分利用我们的时间,CPU也是一样,也要充分利用。
(2)加快响应用户的时间
比如我们经常用的迅雷下载,都喜欢多开几个线程去下载,谁都不愿意用一个线程去下载,为什么呢?答案很简单,就是多个线程下载快啊。
我们在做程序开发的时候更应该如此,特别是我们做互联网项目,网页的响应时间若提升1s,如果流量大的话,就能增加不少转换量。做过高性能web前端调优的都知道,要将静态资源地址用两三个子域名去加载,为什么?因为每多一个子域名,浏览器在加载你的页面的时候就会多开几个线程去加载你的页面资源,提升网站的响应速度。多线程,高并发真的是无处不在。
(3)可以使你的代码模块化,异步化,简单化
例如我们在做 Android程序开发的时候,主线程的UI展示部分是一块主代码程序部分,但是UI上的按钮用相应事件的处理程序就可以做个单独的模块程序拿出来。这样既增加了异步的操,又使程序模块化,清晰化和简单化。
时下最流行的异步程序处理机制,正是多线程、并发程序最好的应用例子。
多线程应用开发的好处还有很多,大家在日后的代码编写过程中可以慢慢体会它的魅力。
二,线程的启动方式
Java中的线程有两种启动方式:
1,类 extends Thread 类.start
public class YuanThread extends Thread{@Overridepublic void run() {super.run();System.out.println("YuanThread run..");}
}
public class Test {public static void main(String[] args) {YuanThread yuanThread =new YuanThread();yuanThread.start();}
}
输出:YuanThread run..
2,类 implements Runnable
public class YuanRunnable implements Runnable{@Overridepublic void run() {System.out.println("YuanRunnable run..");}
}
public class Test {public static void main(String[] args) {YuanRunnable yuanRunnable =new YuanRunnable();Thread thread =new Thread(yuanRunnable);thread.start();}
}
输出:YuanRunnable run..
有人会说还有一种启动方式:
public class YuanCallable implements Callable<String> {@Overridepublic String call() throws Exception {return "YuanCallable";}
}
public class Test {public static void main(String[] args) throws ExecutionException, InterruptedException {YuanCallable callable =new YuanCallable();FutureTask<String> futureTask =new FutureTask<>(callable);Thread thread1 =new Thread(futureTask);thread1.start();System.out.println("YuanCallable="+futureTask.get());}
}
输出:YuanCallable=YuanCallable
那为什么说这不算是一种新的启动方式呢?因为它和第二种方式其本质上是一样的。
我们看FutureTask的源码,可以发现:
它实现了RunnableFuture接口,而RunnableFuture又实现了Runnable接口,所以最终还是
new Thread(Runnable)的方式。 只不过Callable方式有返回值,在获取结果时:
Future.get()方法会阻塞直到返回结果。
三,结束线程的方方式
第一种方式:stop()方式 暴力停止线程,在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。
public class YuanThread extends Thread{@Overridepublic void run() {super.run();while (true){System.out.println("YuanThread run..");}}
}
public class Test {public static void main(String[] args) throws ExecutionException, InterruptedException {YuanThread yuanThread =new YuanThread();yuanThread.start();Thread.sleep(3000);yuanThread.stop();}
}
输出YuanThread run.. 3s后停止
第二种方式:interrupt() +isInterrupt() 停止线程,这种是最常用的方式,根据自己的业务,通过标志位结束线程
public class YuanThread extends Thread{@Overridepublic void run() {super.run();while (!isInterrupted()){System.out.println("YuanThread run..");}}
}
public class Test {public static void main(String[] args) throws ExecutionException, InterruptedException {YuanThread yuanThread =new YuanThread();yuanThread.start();Thread.sleep(3000);yuanThread.interrupt();}
}
注意 :当一个任务在sleep时,去interrupt,会包异常:ava.lang.InterruptedException: sleep interrupted
public class YuanThread extends Thread{@Overridepublic void run() {super.run();while (!isInterrupted()){try {sleep(6000);System.out.println("YuanThread run..");} catch (InterruptedException e) {System.out.println("YuanThread InterruptedException");throw new RuntimeException(e);}}}
}
public class Test {public static void main(String[] args) throws ExecutionException, InterruptedException {YuanThread yuanThread =new YuanThread();yuanThread.start();sleep(2000);yuanThread.interrupt();}
}
四,join wait notify notifyAll
join:放弃当前线程的执行,并返回对应的线程
如果我想让三个线程 T1 ,T2,T3顺序执行,怎么办?
public class T1 extends Thread{@Overridepublic void run() {super.run();for (int i = 0; i < 10; i++) {System.out.println("T1:"+i);}}
}
public class T2 extends Thread{@Overridepublic void run() {super.run();for (int i = 0; i < 10; i++) {System.out.println("T2:"+i);}}
}
public class T3 extends Thread{@Overridepublic void run() {super.run();for (int i = 0; i < 10; i++) {System.out.println("T3:"+i);}}
}
public class Test {public static void main(String[] args) {T1 t1 =new T1();T2 t2 =new T2();T3 t3 =new T3();t1.start();t2.start();t3.start();}
}
输出:
T2:0
T3:0
T1:0
T3:1
T2:1
T3:2
T1:1
T3:3
T2:2
T3:4
T1:2
T3:5
T2:3
T3:6
T1:3
T3:7
T2:4
T3:8
T1:4
T3:9
T2:5
T1:5
T2:6
T1:6
T2:7
T1:7
T2:8
T1:8
T2:9
T1:9
杂乱无章,那如果使用join呢?
public class Test {public static void main(String[] args) throws InterruptedException {T1 t1 =new T1();T2 t2 =new T2();T3 t3 =new T3();t1.start();/**程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,并返回t1线程继续执行直到线程t1执行完毕所以结果是t1线程执行完后,才到主线程执行,相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会*/t1.join();t2.start();t2.join();t3.start();}
}
输出结果:
T1:0
T1:1
T1:2
T1:3
T1:4
T1:5
T1:6
T1:7
T1:8
T1:9
T2:0
T2:1
T2:2
T2:3
T2:4
T2:5
T2:6
T2:7
T2:8
T2:9
T3:0
T3:1
T3:2
T3:3
T3:4
T3:5
T3:6
T3:7
T3:8
T3:9
wait():
1.使当前的线程进行等待
2.释放当前的锁;
3.被唤醒时,重新尝试获取这个锁
wait()结束等待的条件:
1.其他线程(也可以不是线程)调用了该对象的notify或notifyAll方法
2.其他线程(也可以不是线程)调用该等待线程的interrupted方法
3.等待时间超时:wait(有参)
wait是Object方法:
有参数时:wait(500)表示Time_waiting状态
无参数:wait()或wait(0)都表示无限等待:waiting状态
wait的用法:
1.必须配合synchronized使用
2.且使用的必须为同一个对象:synchronized (A)配合A.wait()使用
3.当线程执行到object.wait()时,此线程会同时释放锁synchronized (object);当它结束了wait后,此线程又会重新去争抢锁synchronized (object)。
public class YuanThread extends Thread{private Object object;YuanThread(Object o){object =o;}@Overridepublic void run() {synchronized (object){System.out.println("YuanThread wait before");try {object.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("YuanThread wait after");}}
}
public class Test {public static void main(String[] args) {Object object =new Object();YuanThread yuanThread=new YuanThread(object);yuanThread.start();}
}
输出 YuanThread wait before
会一直等待 直到重新唤醒
notify 必须配合synchronized使用,且在notify后,当前线程不会马上释放锁,要等到当前线程被synchronized修饰的代码执行完,才会释放锁。
使用notify的对象为A,不能唤醒B.wait的线程
notify会随机唤醒一个wait的线程
notifyAll 唤醒所有wait的线程,让这些被唤醒的线程去争抢,按争抢顺序依次执行
public class T1 extends Thread{private Object object;T1(Object o){object =o;}@Overridepublic void run() {synchronized (object){object.notify();}}
}
public class Test {public static void main(String[] args) throws InterruptedException {Object object =new Object();YuanThread yuanThread=new YuanThread(object);yuanThread.start();T1 t1 =new T1(object);t1.start();}
}
输出:
YuanThread wait before
YuanThread wait after
注意:1,调用等待线程的interrupted方法,会抛出异常,然后继续执行wait后的代码
2,当时间超时,会自动恢复执行,会重新去获得锁,然后从wait的下一行开始执行
五,线程的状态切换
线程的状态切换,主要是下面这个图所示:
当线程start的时候,它并不会立即执行,而是会进入一个就绪状态,等待CPU时间片轮转到该任务时,才会运行。就绪和运行中我们统称为运行态。
六,锁
6.1对象锁与类锁
对象锁:synchronized 修饰非静态的方法和synchronized(this)都是使用的对象锁,一个系统可以有多个对象实例,所以使用对象锁不是线程安全的,除非保证一个系统该类型的对象只会创建一个(通常使用单例模式)才能保证线程安全;
public class YuanThread extends Thread{private Object object;YuanThread(Object o){object =o;}@Overridepublic void run() {synchronized (object){test1(object);}}public void test1(Object object){synchronized (object){System.out.println("dui xiang suo");}}
}
public class Test {public static void main(String[] args) throws InterruptedException {Object object =new Object();YuanThread yuanThread=new YuanThread(object);yuanThread.start();}
}
类锁:锁是加持在类上的,用synchronized static 或者synchronized(class)方法使用的锁都是类锁,因为class和静态方法在系统中只会产生一份,所以在单系统环境中使用类锁是线程安全的;
public class YuanThread extends Thread{private Object object;YuanThread(Object o){object =o;}@Overridepublic void run() {test1();test2();}public static synchronized void test1(){System.out.println("lei suo 1");}public static synchronized void test2(){synchronized (YuanThread.class){System.out.println("lei suo 2");}}
}
6.2显示锁
前面提到的synchronized是内置锁,是Java语言提供的一种基本锁机制,当代码块使用synchronized关键字进行同步时,会自动获取对象的内置锁,执行完该代码块后会自动释放锁。
显示锁,也称为可重入锁,是Java语言提供的一种高级锁机制。与内置锁不同的是,显示锁需要程序员手动获取和释放锁。
显示锁都是由java.util.concurrent.locks.Lock派生出来的,我们看一下这个类的方法:
再看一下他的实现类:
以ReentrantLock为例,我们看一下显示锁的使用:
public class YuanThread extends Thread{private Lock lock;YuanThread(Lock lock){this.lock =lock;}@Overridepublic void run() {lock.lock();try {System.out.println("run lock");}finally {lock.unlock();}}}
public class Test {public static void main(String[] args) throws InterruptedException {Lock lock =new ReentrantLock();YuanThread yuanThread=new YuanThread(lock);yuanThread.start();}
}
上面在使用中,如果我们忘记了在finally里面释放掉锁的话,就很有可能会导致一些死锁的情况。
trylock就是尝试获取锁,如果锁已经被其他线程占用那么立即返回false,如果没有那么应该占用它并返回true,表示拿到锁啦。
trylock方法里带了参数的,这个参数的作用是指定一个时间,表示在这个时间内一直尝试去获得锁,如果到时间还没有拿到就放弃。
因为trylock对锁并不是一直阻塞等待的,所以可以更多的规避死锁的发生。
lockInterruptibly是在线程获取锁时优先响应中断,如果检测到中断抛出中断异常由上层代码去处理。这种情况下就为一种轮循的锁提供了退出机制。
再来看一下另外一个显示锁,ReentrantReadWriteLock可重入的读-写锁
什么是读写锁呢?比如一波数据大部分时候都是提供读取的,而只有比较少量的写操作,那么如果用互斥锁的话就会导致线程间的锁竞争。如果对于读取的时候大家都可以读,一旦要写入的时候就再将某个资源锁住。这样的变化就很好的解决了这个问题,使的读操作可以提高读的性能,又不会影响写的操作。
下面来看一下使用:
databean 数据类
public class DataBean implements Serializable {private String data;public DataBean(String data) {this.data = data;}public String getData() {return data;}public void setData(String data) {this.data = data;}
}
读写锁:
public class YuanLock {private ReentrantReadWriteLock lock =new ReentrantReadWriteLock();private Lock readLock =lock.readLock();//读取锁private Lock writeLock =lock.writeLock();//写入锁private DataBean bean;public YuanLock(DataBean bean) {this.bean = bean;}public DataBean getData(){readLock.lock();try {System.out.println("-----readlock ="+bean.getData());return bean;}finally {readLock.unlock();}}public void setData(String data){writeLock.lock();try {System.out.println("-----writelock :"+data);bean.setData(bean.getData()+data);}finally {writeLock.unlock();}}}
读取线程:
public class T1 extends Thread{private YuanLock lock;T1(YuanLock o){lock =o;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {//读取十次for (int j = 0; j < 5; j++) {System.out.println(lock.getData());}}}
}
写入线程:
public class T2 extends Thread{private YuanLock lock;T2(YuanLock o){lock =o;}@Overridepublic void run() {super.run();for (int i = 0; i < 5; i++) {//写入五次lock.setData(i+"");}}
}
具体应用:
public class Test {public static void main(String[] args) throws InterruptedException {Lock lock =new ReentrantLock();YuanThread yuanThread=new YuanThread(lock);yuanThread.start();DataBean bean =new DataBean("-");YuanLock yuanLock =new YuanLock(bean);for (int i = 0; i < 2; i++) {for (int j = 0; j < 5; j++) {T1 t1 =new T1(yuanLock);t1.start();}T2 t2 =new T2(yuanLock);t2.start();}}
}
6.3可重入锁
什么是可重入锁?前面我们说的synchronized 和 ReentrantLock 都是可重入锁。
可重入锁就是可以重复进入的锁,也叫递归锁。前提是同一把锁,如同一个类、同一个实例、同一个代码块。可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
可重入锁的意义之一在于防止死锁。
实现原理实现是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。
如果同一个线程再次请求这个锁,计数器将递增;
每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。
synchronized 使用:
public class T3 extends Thread{private Object object;public T3(Object object) {this.object = object;}@Overridepublic void run() {synchronized (object){System.out.println("first");synchronized (object){System.out.println("second");}}}
}
public class Test {public static void main(String[] args) throws InterruptedException {T3 t3 =new T3(new Object());t3.start();}
}
lock使用:
public class YuanThread extends Thread{private Lock lock;YuanThread(Lock lock){this.lock =lock;}@Overridepublic void run() {lock.lock();try {System.out.println("first");lock.lock();try {System.out.println("second");}finally {lock.unlock();}}finally {lock.unlock();}}
}
public class Test {public static void main(String[] args) throws InterruptedException {Lock lock =new ReentrantLock();YuanThread yuanThread=new YuanThread(lock);yuanThread.start();}
}
6.4死锁
死锁就是两个或两个以上的线程持有不同系统资源的锁,线程彼此都等待获取对方的锁来完成自己的任务,但是没有让出自己持有的锁,线程就会无休止等待下去。线程竞争的资源可以是:锁、网络连接、通知事件,磁盘、带宽,以及一切可以被称作“资源”的东西
class DieLockThread extends Thread {private boolean flag;public DieLockThread(boolean flag) {this.flag = flag;}@Overridepublic void run() {int i = 0;int j = 0;if (flag) {while (true) {synchronized (Lock.LOCK1) // 使用第一把锁{synchronized (Lock.LOCK2) // 使用第二把锁{System.out.println("一一一一一一一一一一一一" + i++);}}}} else {while(true) {synchronized (Lock.LOCK2) // 使用第二把锁{synchronized (Lock.LOCK1) // 使用第一把锁{System.out.println("二二二二二二二二二二二二" + j++);}}}}}
}
class Lock {public final static Object LOCK1 = new Object();public final static Object LOCK2 = new Object();
}
public class DieLockDemo {public static void main(String[] args) {new DieLockThread(true).start();new DieLockThread(false).start();}}
七,ThreadLocal
ThreadLocal 是线程本地变量。ThreadLocal可以让每个线程拥有一个属于自己的变量的副本,不会和其他线程的变量副本冲突,实现了线程的数据隔离。
下面看下使用:
public class UseThreadLocal {static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {@Overrideprotected Integer initialValue() {return 1;}};/*** 运行3个线程*/public void StartThreadArray() {Thread[] runs = new Thread[3];for (int i = 0; i < runs.length; i++) {runs[i] = new Thread(new TestThread(i));}for (int i = 0; i < runs.length; i++) {runs[i].start();}}public static void main(String[] args) {UseThreadLocal test = new UseThreadLocal();test.StartThreadArray();}
}
public static class TestThread implements Runnable {int id;public TestThread(int id) {this.id = id;}public void run() {System.out.println(Thread.currentThread().getName() + ":start");// 如果使用了 ThreadLocal 会单独Copy一份 到 当前线程 例如 Thread-0Integer s = threadLocal.get();s = s + id;threadLocal.set(s); System.out.println(Thread.currentThread().getName() + " :"+ threadLocal.get());}
}
八,CAS
什么是CAS呢?CAS是compare and swap的缩写,中文翻译成比较并交换。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作,直到成功为止。
下面这个图可以看出什么是CAS操作:
CAS都是进行的原子操作,那什么是原子操作呢?
假定有两个操作A和B(A和B可能都很复杂),如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。
实现原子操作可以使用锁,锁机制,满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁,这里会有些问题:首先,如果被阻塞的线程优先级很高很重要怎么办?其次,如果获得锁的线程一直不释放锁怎么办?(这种情况是非常糟糕的)。还有一种情况,如果有大量的线程来竞争资源,那CPU将会花费大量的时间和资源来处理这些竞争,同时,还有可能出现一些例如死锁之类的情况,最后,其实锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重。
CAS实现原子操作的三大问题:
1,ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA问题。
2,循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
3,只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
九,原子操作类
9.1AtomicInteger
int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
9.2AtomicIntegerArray
主要是提供原子的方式更新数组里的整型,其常用方法如下。
int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。
需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。
9.3AtomicReference
原子更新引用类型。
9.4AtomicStampedReference
利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA问题了。这就是AtomicStampedReference的解决方案。AtomicMarkableReference跟AtomicStampedReference差不多, AtomicStampedReference是使用pair的int stamp作为计数器使用,AtomicMarkableReference的pair使用的是boolean mark。 还是那个水的例子,AtomicStampedReference可能关心的是动过几次,AtomicMarkableReference关心的是有没有被人动过,方法都比较简单。
9.5AtomicMarkableReference
原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。
十,阻塞队列
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。
什么是阻塞队列?
1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序整体处理数据的速度。
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。
为了解决这种生产消费能力不均衡的问题,便有了生产者和消费者模式。生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。
抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException("Queuefull")异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null。
一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。
常用的阻塞队列:
①ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证线程公平的访问队列,所谓公平访问队列是指阻塞的线程,可以按照阻塞的先后顺序访问队列,即先阻塞线程先访问队列。非公平性是对先等待的线程是非公平的,当队列可用时,阻塞的线程都可以争夺访问队列的资格,有可能先阻塞的线程最后才访问队列。初始化时有参数可以设置
②LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。
③PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
④DelayQueue:一个使用优先级队列实现的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
DelayQueue非常有用,可以将DelayQueue运用在以下应用场景。
缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
⑤SynchronousQueue:一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合传递性场景。
⑥LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
多了tryTransfer和transfer方法,
(1)transfer方法
如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。
(2)tryTransfer方法
tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回,而transfer方法是必须等到消费者消费了才返回。
⑦LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
所谓双向队列指的是可以从队列的两端插入和移出元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法,以First单词结尾的方法,表示插入、获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法,表示插入、获取或移除双端队列的最后一个元素。另外,插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,不知道是不是JDK的bug,使用时还是用带有First和Last后缀的方法更清楚。在初始化LinkedBlockingDeque时可以设置容量防止其过度膨胀。另外,双向阻塞队列可以运用在“工作窃取”模式中。
以上的阻塞队列都实现了BlockingQueue接口,也都是线程安全的。