文章目录
9、生产者与消费者应用案例
sleep() : 让线程进入休眠状态,让出CPU的时间片,不释放监视器的所有权(对象锁)
wait() : 让线程进入等待状态,让出CPU的时间片,释放监视器的所有权,等待其他线程通过notify方法来唤醒
10、线程生命周期
调用start() 方法进入就绪状态,获取到CPU的时间片,调用run()方法会进入运行状态,
如果运行状态调用yield() 方法,会进入就绪,让出一片时间片,
调用join() 和sleep() 方法会进入blocked 状态,join()或sleep() 结束会重新进入runnable
runnable方法调用wait 也会进入blocked 状态, 阻塞状态会进入锁定状态,
wait() 的blocked 状态的通过调用notify() 进入锁定
11、线程池
JDK1.5 后,提供了线程的线程池,Java 里面线程池的顶级接口使Executor,是一个执行线程的工具
线程池接口是ExecutorService。
java.util.concurrent 包,并发编程中很常用的使用工具
Executor 接口:
执行已提交的Runnable 任务的对象
ExecutorService 接口
提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状态而生成Future的方法
Executors类:
此包中所定义的Executor、 ExecutoService 等工厂和使用方法
newSingleThreadExecutor
创建一个单线程的线程池,这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,此线程池保证所有任务的执行顺序按照任务的提交顺序执行
newFixedThreadPool
创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
线程池的大小一旦达到最大值就会保持不变
如果某个线程因为执行异常而结束,那么线程池会补充一个新的线程
newCachedThreadPool
创建一个可缓存的线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行)的线程,当任务数增加时,此线程池又可能智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(JVM)能够创建的最大线程大小。(比较少用,)
newScheduledThreadPool
创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求
public static void main(String[] args){
// ExecutorService es = Executors.newSingleThreadExecutor();
ExecutorService es = Executors.newFixedThreadPool(3);
es.execute(new MyRunnable6());
es.execute(new MyRunnable6());
es.shutdown();
}
附加
public class Test1 {
private int a=1, b=2;
public void foo(){ // 线程1
a=3;
b=4;
}
public int getA(){ // 线程2
return a;
}
public int getB(){ // 线程2
return b;
}
b=4语句可能比a=3语句先执行,
- java 编译器的重排序(Reording)操作有可能导致执行顺序和代码顺序不一致
假设有两条语句,代码执行顺序是语句1先于语句2,那么只要语句2不依赖于语句1,打乱它们的顺序对最终结果没有影响的话,那么它们的顺序没有限制。
- 从线程工作内存写回主存时顺序无法保证,
JVM中主存和线程工作内存之间的交互,线程再修改一个变量时,先拷贝入线程工作内存,在线程工作内存修改后写回主存。线程1 把变量写回Main Memery 的过程对线程2的可见性顺序无法保证。Jvm 中一个重要的问题就是如何让多个线程之间,对象的状态对于各线程的”可视性“顺序一致,它的解决方法就是Happens-before 规则:要想保证执行动作B的线程看到动作A的结果,A和B之间就必须满足happens-before
java1.5之前Java 中的锁只有基本的synchronized, java 5之后,增加了一些其他锁,比如ReentranLock,它基本作用和synchronized 相似,但提供了更多的操作方式,比如在获取锁时不必向sybchronized 那样傻等,可以设置定时,轮询,或者中断,使得他在获取多个锁的情况可以避免死锁的操作。
java 1.5 ReentranLock 的性能比sybchronized 来说有很大提高,java1.6对sybchronized 进行了优化,现在两者差不多,ConcurrentHashMap 中,每个哈市区间使用的锁正是ReentranLock,1.8后ConcurrentHashMap 使用的又是sybchronized 了。
不变模式(immutable) 是多线程安全里最简单的一种保障方式,主要通过final关键字来限定。
HashTable 容器在竞争激烈的并发环境表现出效率低下的原因,是所有访问HashTable 的线程必须竞争同一把锁,假如每一把锁用于锁住容器中的一部分,那么当多线程访问容器中不同部分的时候,就不会存在锁的竞争。
ConcurrentHashMap 由Segment 数组结构和HashEntry 数组结构组成,Segment 是一种锁,HashEntry 用于存储键值对。每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
java 内存模型与volatile关键字
A,B两个线程如果要实现通信的话,必须实现两个步骤:
- 线程A把本地内存A中更新过的共享变量刷新到主内存中
- 线程B读取主内存中共享变量,拷贝一份副本到本地内存B
volatile 关键字语义:
- 任何一个线程对该变量进行修改后,其他线程都立即可见,保证了可见性
- 禁止指令重新排序,保证了有序性
保证可见性的过程如下:
- 使用volatile关键字会强制更新后的共享数据立即刷新到主内存
- 当其他线程修改了被volatile修饰的共享数据,此线程上的工作内存上该数据的副本会失效,强制此线程去主内存读取更新后的数据
保证有序性:
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
语句1和语句2一定在语句3前面,语句4和语句5一定在语句3后面。
synchronized 和volatile 的比较
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
volatile 的内存语义:如果一个变量被volatile 关键字修饰时,那么对这个变量写的就是本地内存中拷贝刷新到共享内存中,对这个变量的读会有一些不同,读的时候是无视他的本地内存的拷贝,只是从共享变量中读取数据
synchronized: 读和写是加锁的,别的线程无法对这个变量进行读写操作
public class RunThread extends Thread {
private boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void setRunFlag(boolean flag) {
isRunning = flag;
}
@Override
public void run() {
System.out.println("I'm come in...");
boolean first = true;
while(isRunning) {
if (first) {
System.out.println("I'm in while...");
first = false;
}
}
System.out.println("I'll go out.");
}
//
public class MyRun {
public static void main(String[] args) throws InterruptedException {
RunThread thread = new RunThread();
thread.start();
Thread.sleep(100);
thread.setRunFlag(false);
System.out.println("flag is reseted: " + thread.isRunning());
}
// 虽然有thread.setRunFlag(false);, 但是并不会happens-before,虽然主线程中对isRunning 进行了修改,然而对子线程中的while 来说,并没有改变,所以这就引发在while 中的死循环
//虽然对象以及成员变量分配的内存是在共享内存中的,不过对于每个线程而言,还是可以拥有这个对象的拷贝,这样做的目的是为了加快程序的执行,这也是现代多核处理器的一个显著特征。从上面的内存模型可以看出,Java的线程是直接与它自身的工作内存(本地内存)交互,工作内存再与共享内存交互。这样就形成了一个非原子的操作
//这里工作内存被 while 占用,无法去更新主线程对共享内存 isRunning 变量的修改。所以,如果我们想要打破这种限制,可以通过 volatile 关键字来处理。通过 volatile 关键字修饰 while 的条件变量,即 isRunning。
// 修改成这样
private volatile boolean isRunning = true;
这样子线程每次读取isRunning, 就去主内存中读取, 不再工作内存中读
volatile 原子性测试
public class DemoNoProtected {
static class MyThread extends Thread {
static int count = 0;
private static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("count = " + count);
}
@Override
public void run() {
addCount();
}
}
public static void main(String[] args) {
MyThread[] threads = new MyThread[100];
for (int i = 0; i < 100; i++) {
threads[i] = new MyThread();
}
for (int i = 0; i < 100; i++) {
threads[i].start();
}
}
}
/*output
count = 300
count = 300
count = 300
count = 400
... ...
count = 7618
count = 7518
count = 9918
*/
即使我们添加了volatile 关键字, 也不行,甚至最后输出的不是10000
因为我们都知道,count++ 是这样的
int tmp = count;
tmp = tmp + 1;
count = tmp;
可见,count++ 不是原子性操作
ThreadLocal
ThreadLocal 修饰的变量一般称为线程本地变量,特殊的线程绑定机制,将对象的可见范围限制在同一线程内,
public class Solution {
public static void main(String[] args) {
TestDemo d1 = new TestDemo();
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d1);
Thread t3 = new Thread(d1);
Thread t4 = new Thread(d1);
Thread t5 = new Thread(d1);
Thread t6 = new Thread(d1);
t3.start();
t1.start();
t2.start();
t4.start();
t6.start();
t5.start();
}
}
class TestDemo implements Runnable {
ThreadLocal<Integer> a = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 0;
}
};
@Override
public void run() {
a.set(a.get()+1);
System.out.println(Thread.currentThread().getName()+","+a.get());
}
可以看到ThreadLocal中有一个静态内部类ThreadLocalMap;
其实从get()方法中你就可以看出,该map是根据每个线程存一个值来保存变量副本的。
锁池: 假设线程A已经拥有了某个对象的锁,而其他线程想要调用这个对象的某个sybchronized方法,由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就会进入了该对象的锁池中。(记录着被阻塞的线程,也就是想用加锁资源的线程)
等待池: 假设一个线程A调用了某个对象的wait() 方法,线程A就会释放该对象的锁,进入到该对象的等待池中
notify 和notifyAll 有什么区别:
- 如果线程调用了对象的wait() 方法,那么线程便会处于对象的等待池中,等待池中的线程不会去竞争该对象的锁
- 当有线程调用了对象的notifyAll() 方法(唤醒所有的wait线程)或notify() 方法(只随机唤醒一个wait线程),被唤醒的线程便会进入该对象的锁池中,锁池中的线程会去经侦该对象锁,也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll 会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
- 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用wait() 方法,它才会重新回到等待池中,而竞争到对象锁的线程则继续往下执行,直到执行完了sybchronized代码块,它会释放该对象锁,这时锁池中的线程会继续竞争该对象锁。
综上,所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。
有了这些理论基础,后面的notify可能会导致死锁,而notifyAll则不会的例子也就好解释了
信号量 semaphore
(1)如果一个线程要访问一个共享资源,他必须先获得信号量。如果信号量的内部计数器大于0,信号量将减1,然后允许访问这个共享资源。计数器大于0意味着又可以使用的资源,因此线程讲被允许使用其中一个资源。
(2)如果信号量等于0,信号将将会把线程植入休眠直到计数器大于0.计数器等于0的时候意味着所有的共享资源已经被其他线程使用了,所以需要访问这个共享资源的线程必须等待。
(3)当线程使用完这个共享资源时,信号量必须被释放,以便其他线程能够访问共享资源,释放操作将使用信号量的内部计数器增加1。
并发集合
java提供了两类适用于并发应用的集合
阻塞式集合: 当集合已满或为空,添加或移除方法不能立即执行,那么调用这个方法的线程会被阻塞,一直到方法可以被成功执行
非阻塞式集合: 当集合已满或为空,添加或移除方法不能立即执行,会返回null 或抛出异常,不会被阻塞
非阻塞式列表对应的实现类:ConcurrentLinkedDeque
阻塞式列表对应的实现类:LinkedBlockingDeque
用于数据生成或者消费的阻塞式列表对应的实现类:LinkedTransferQueue
按优先级排序列表元素的阻塞式列表对应的实现类:PriorityBlockingQueue
带有延迟列表元素的阻塞式列表对应的实现类:DelayQueue
非阻塞式列表可遍历映射对应的饿实现类:ConcurrentSkipListMap
随机数字对应的实现类:ThreadLockRandom
原子变量对应的实现类:AtomicLong和AtomicIntegerArray
JDK常用的包
java.lang(唯一一个不需要导入的包) java.io java.net java.util java.sql
String s = “a”+“b”+“c”+“d”;共创建了多少个对象?
1个对象。由于编译器对字符串常量直接相加的表达式进行了优化。编译时即可去掉+号,直接将其编译成一个常量相连的结果。