并发、线程简单理解:
一、同步计数器:
1、CountDownLatch同步计数器:
- 倒计数门闩,在完成一组正在其他线程中执行得操作之前,它允许一个或多个线程一直等待,在计数器到达0之前,
await
方法会一直阻塞;之后会释放所有等待线程,await
的所有后续调用都将返回; - 方法:
CountDownLatch(int count);//构造用给定计数初始化的同步计数器;
void await();//使当前线程在计数器至0前一直等待,除非被中断;
boolean await(long timeout,TimeUnit unit);//使当前线程在计数器倒计数至0前一直等待,除非被中断或超时
void countDown();//计数器减一,如果到达0,则释放所有等待的线程;
long getCount();//返回当前计数;
- 使用场景:
- 开5个线程去下载,当5个线程都执行完才算下载成功;
- 多个线程上传文件,只有当每个文件都上传成功才算成功;
- 示例:
public class CountDownLatchDemo {
static class Worker extends Thread{
private String workerName;
private CountDownLatch latch;
public Worker(String workerName,CountDownLatch latch){
this.workerName = workerName;
this.latch = latch;
}
@Override
public void run() {
try {
System.out.println("Thread "+ this.workerName+" is begin!");
Thread.sleep(2000l);
System.out.println("Thread "+ this.workerName+" is end!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
latch.countDown();
}
}
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
Worker worker1 = new Worker("11111",latch);
Worker worker2 = new Worker("22222",latch);
Worker worker3 = new Worker("33333",latch);
worker1.start();
worker2.start();
worker3.start();
latch.await();
System.out.println("main end");
}
}
//运行结果:
Thread 11111 is begin!
Thread 33333 is begin!
Thread 22222 is begin!
Thread 11111 is end!
Thread 22222 is end!
Thread 33333 is end!
main end
2、CyclicBarrier同步计数器:
- 循环屏障允许一组线程互相等待,直到到达某个公共屏障点,然后所有的这组线程再同步往后执行,因为该
barrier
在释放等待线程中可以重用,所以叫做循环的barrier
; - 方法:
CyclicBarrier(int parties);//创建一个新的循环屏障,它将在给定数量的线程处于等待状态在启动,但不会在启动barrier时进行预定义的操作;
CyclicBarrier(int parties,Runable barrierAction);//创建一个新的循环屏障,它将在给定数量的线程处于等待状态在启动,并在启动barrier时执行给定的屏障操作barrierAction,该操作有最后一个进入barrier的线程执行;
int await();//在所有参与者都已经在此barrier上调用await方法之前,将一直等到;
int await(long timeout,TimeUnit unit);//在所有参与者都已经在此屏障上调用await方法之前将一直等待,或者超出了指定的等待时间;
int getNumberWaiting();//返回当前在平长出等待的线程数目;
int getParties();//返回要求启动此barrier的线程数目;
void reset();//将循环屏障重置为初始章台;
- 示例:
public class CyclicBarrierDemo {
static class Worker extends Thread{
private String workerName;
private CyclicBarrier barrier;
public Worker(String workerName,CyclicBarrier barrier){
this.workerName = workerName;
this.barrier = barrier;
}
@Override
public void run() {
try {
System.out.println("Thread "+ this.workerName+" is begin!");
Thread.sleep(2000l);
System.out.println("Thread "+ this.workerName+" is end!");
barrier.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}
}
static class TotalTask extends Thread{
@Override
public void run() {
System.out.println("所有线程都到达barrier");
}
}
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3,new TotalTask());
Worker worker1 = new Worker("11111",barrier);
Worker worker2 = new Worker("22222",barrier);
Worker worker3 = new Worker("33333",barrier);
worker1.start();
worker2.start();
worker3.start();
System.out.println("main end");
}
}
//运行结果:
main end
Thread 11111 is begin!
Thread 33333 is begin!
Thread 22222 is begin!
Thread 11111 is end!
Thread 33333 is end!
Thread 22222 is end!
所有线程都到达barrier
3、CountDownLatch与CyclicBarrier区别:
CountDownLatch
:一个线程等待另外N个线程完成某个事情之后继续执行,重点是一个线程等待;CyclicBarrier
:N个线程互相等待,任何一个线程完成之前,所有线程都必须等待;
4、Semaphore同步计数器:
- 是一个计数信号量,维护一个许可集合,在许可可用前会阻塞每一个
acquire
,等待获取许可;release()
释放当前占用的许可,允许其他阻塞的线程获得; - 方法:
Semaphore(int permits);//创建具有给定许可数目、非公平的Semphore对象;
Semaphore(int permits,boolean fair);//创建具有给定许可数目、公平的semphore对象,所谓公平性就是先来先服务FIFO;
void acquire();//从信号量获取一个许可,在获取到许可之前线程将被阻塞;
int acailablePermits();//返回此信号量中的可用许可数目
void release();//释放当前许可
- 示例:
public class SemaphoreDemo {
static class Worker extends Thread{
private String workerName;
private Semaphore semaphore;
public Worker(String workerName,Semaphore semaphore){
this.workerName = workerName;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
Thread.sleep(1000l);
semaphore.acquire();
System.out.println("Thread "+ this.workerName+" 获取许可!");
Thread.sleep(2000l);
semaphore.release();
System.out.println("Thread "+ this.workerName+" 释放许可");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 12; i++) {
Worker worker = new Worker("worker"+i, semaphore);
worker.start();
}
System.out.println("main thread end");
}
}
//运行结果:
main thread end
Thread worker10 获取许可!
Thread worker11 获取许可!
Thread worker7 获取许可!
Thread worker10 释放许可
Thread worker2 获取许可!
Thread worker3 获取许可!
Thread worker0 获取许可!
Thread worker11 释放许可
Thread worker7 释放许可
Thread worker3 释放许可
Thread worker0 释放许可
Thread worker2 释放许可
Thread worker6 获取许可!
Thread worker1 获取许可!
Thread worker9 获取许可!
Thread worker6 释放许可
Thread worker8 获取许可!
Thread worker4 获取许可!
Thread worker5 获取许可!
Thread worker9 释放许可
Thread worker1 释放许可
Thread worker4 释放许可
Thread worker5 释放许可
Thread worker8 释放许可
二、线程属性:
1、id:
线程唯一标识,自动生成,不允许修改;
线程初始化方法init()
会给线程设置id,该id通过synchronized
标记的 nextThreadID()
方法获取,id自增;
2、name:
线程的名称,可以自定义成具有具体含义的名字,便于识别不同作用的线程,可重复;
如果没有指定线程的名称,默认是“Thread-”+nextThreadNum()
;
3、idDaemon:
- 是否守护线程,
true
:守护线程,false
:用户线程; - 当JVM中所有线程都是守护线程,JVM将退出;
- 具有代表性的线程:main 用户线程,gc线程:守护线程;
- 子线程会默认继承父线程这个属性;
- 必须在线程
start()
之前设置这个属性,线程运行中设置线程守护属性会抛出异常;
4、priority:
- 线程优先级,线程优先级高的概率上会优先执行,并不可靠;
- 线程优先级有10个,默认是5,且子线程会继承父线程的优先级;
- 程序不应该依赖优先级,一般默认设置成5;
三、线程状态:
1、初始(NEW):
新创建一个线程对象,但还没有调用start()
方法;
2、运行(RUNNABLE):
Java线程中将就绪(ready
)和运行中(running
)两种笼统的称为运行,线程创建后,其他线程调用了该对象的start()
方法,该状态位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready
),就绪状态的线程在获得CPU时间片变为运行中状态(running
);
3、阻塞(BLOCKED):
表示线程阻塞于锁;
4、等待(WAITING):
进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断);
5、超时等待(TIMED_WAITING):
该状态不同于等待,它可以在指定的时间后自行返回;
6、终止(TERMINATED):
表示该线程已经执行完毕;
7、同步队列状态:
- 当前线程想调用对象a的同步方法时,发现对象a的锁被别的线程占有,此时当前线程进入同步队列,同步队列里面放的都是想争夺对象锁的过程;
- 当线程1被另外一个线程2唤醒时,1线程进入同步队列,去争夺对象锁;
- 同步队列是在同步的环境下才有的概念,一个对象对应一个同步队列;
- 线程等待时间到了或被
notify/notifyAll
唤醒后,回进入同步队列竞争锁,如果获得锁,进入RUNNANBLE
状态,否则进入BLOCKED
状态等待获取锁;
8、方法比较:
Thread.sleep(long millis);//一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放锁,millis后线程自动苏醒进入就绪状态,作用:给其他线程执行机会的最佳方式;
Thread.yield();//一定是当前线程调用此方法,当前线程放弃获取CPU时间片,但不释放锁资源,有运行状态变为就绪状态,让OS再次选择线程,作用:让相同优先级的线程轮流执行,但不保证一定会轮流执行,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中,Thread.yield()不会导致阻塞,该方法于sleep类似,只是不能有用户指定暂停多长时间;
thread.join()/thread.join(long millis);//当前线程里调用其他线程t 的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁,线程 t 执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态;
obj.wait();//当前线程调用对象的wait()方法,当前对象释放锁,进入等待队列,依靠notify()/notifyAll()唤醒,或设置过期时间唤醒;
obj.notify();//唤醒在对象监视器上等待的单个线程,选择是任意性的;
LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.partUntil(long deadlines);//当前线程进入WAITING/TIMED_WAITING状态,对比wait方法,不需要获取锁就能让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒;
四、线程之间的协作:
当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其他部分之前完成,那么就需要对线程进行协调;
1、使用join()协调:
-
后台线程:
- 所谓后台(
daemon
)线程,是指在程序运行的时候在后台提供的一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会kill掉进程中所有的后台线程。 - 必须在线程启动之前调用
setDaemon()
方法,才能将线程设置为后台线程。 - 如果一个线程为后台线程,那么在该线程中创建的任何线程都会自动设置成后台线程。
- 需要注意的是,后台线程中的
finally
子句并不会执行,原因在于当最后一个非后台线程终止时,后台程序会突然终止,JVM会立即关闭所有后台进程并且是以一种粗暴的形式进行关闭。 - 非后台的
Executor
通常是一种更好的方式,因为Executor
控制的所有任务可以同时被关闭,并且关闭是有序的。
- 所谓后台(
-
直接继承Thread实现线程:
在非常简单的情况下,可以直接从Thread
继承的方式来实现线程。事实上,实现接口的方式会更好一些,原因在于:
Java不支持多重继承,因此继承了Thread类就无法继承其他类,但可以实现多个接口;
类可能只要求执行就行,继承整个Thread类开销过大。 -
加入一个线程:
- 一个线程可以在其他线程之上调用
join()
方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程t上调用t.join()
,此线程将被挂起,直到目标线程t结束才恢复。 - 可以在调用
join()
时带上一个超时参数,这样如果目标线程在这段时间到期时还没有结束的话,join()
方法总能返回。 - 对
join()
方法的调用可以被中断,做法是在调用线程上调用interrupt()
方法,这时需要用到try-catch
子句。
- 一个线程可以在其他线程之上调用
2、wait()、notify()、notifyAll():
- 调用
wait()
使得线程等待某个条件满足,线程在等待时被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用notify()
或者notifyAll()
来唤醒挂起的线程; wait()、notify()、notifyAll()
都是属于Objec
t的一部分,而不是属于Thread
,并且只能用在同步方法或同步控制快中,否则在运行时会抛出异常;- 使用
wait()
挂起期间,线程会释放锁,如果不释放锁,那么其他线程就无法进入对象的同步方法或同步控制块中,也就无法执行notify()、notifyAll()
来唤醒挂起的线程,造成死锁;
wait()和sleep()的区别:
wait()
是Object
的方法,sleep()
是Thread
的静态方法;wait()
会释放锁,sleep()
不会;join()
是通过wait()
实现的,因此join()
也能释放锁;notify() 和 notifyAll()的区别:
对于多个任务的单个对象处于
wait()
状态,调用notifyAll()
方法比调用notify() 方法更安全,notify() 方法在众多等待同同一个锁的任务只有一个会被唤醒,因此除非能够保证被唤醒的任务是恰当的任务,否则线程调度的非确定性,可能会导致死锁,而notifyAll()则是唤醒所有等待同一个锁的所有任务;(事实上notifyAll() 真正唤醒的是等待该条件的任务,而不是所有任务 )
3、await()、signal()、signalAll():
可以在Condition
上调用await()
方法使线程等待,其他线程调用signal()
或signalAll()
方法唤醒等待的线程,await()
可以指定等待的条件;
五、死锁:
两个或两个以上的进程因为争夺资源造成的一种等待的过程就是死锁,如果没有外力干涉,就不可能再进行下去;
1、验证是否死锁:
使用jps
和jstack
命令:
2、死锁产生的必要条件:
- 1、互斥条件:只有对必须互斥使用的资源的争夺才会导致死锁
- 2、不剥夺条件:进程所获得的资源在未使用完之前,不能有其他进程强行夺走,只能主动释放
- 3、请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求的进程被阻塞,但又对自己已有的资源保持不放。
- 4、循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程请求。
3、什么时候发生死锁:
- 对系统资源的竞争;
- 进程推进顺序非法;
- 信号量的使用不当也会发生死锁;
4、预防死锁:
- 破坏互斥条件:只有对必须互斥使用的资源的争夺才会导致死锁,将只能互斥使用的资源改造成允许共享使用,系统就不会进入死锁状态;
- 缺点:并不是所有的资源都可以改造成共享使用的资源,并且为了系统安全,很多地方必须使用这种互斥性,很多时候都无法破坏互斥条件;
- 破坏不剥夺条件:
方案一:当某个进程请求新的资源得不到满足时,它必须释放保持的所有资源,待以后需要时在重新申请,即使某些资源尚未使用完就需要主动释放,从而破坏不可剥夺条件;
方案二:当某个进程需要的资源一直被其他进程所占用的时候,可以由操作系统协助,将想要的资源强行剥夺,需要考虑优先级;- 缺点:实现较为困难;释放以获得的资源可能造成前一段工作的失效,因此一般适用于易保存和恢复状态的资源;反复的申请和释放资源都会增加系统开销,降低系统吞吐量;若采用方案一,意味着只要暂时得不到某个资源,之前获得的资源就需要放弃,以后在重新申请,如果一直发生这样的是就会导致进程饥饿;
- 破坏请求和保持条件:可以采用静态分配策略,进程在运行前一次性申请它所需要的所有资源,在他的资源满足前,不让他投入运行,一旦开始运行,这些资源就一直归他所有,这样就不会请求别的任何的资源;
- 缺点:有些资源可能只需要用很短的时间,因此如果进程的整个运行期都一直保持这所有资源,就会造成验证的资源浪费,资源利用率很低;
- 破坏循环等待条件:使用顺序资源分配法,先给系统的资源编号,规定每个进程必须按照标号递增的顺序请求资源,同类资源一次申请完;
原理分析:一个进程只有占用小标号的资源时,才有资格申请更大的资源,已持有的大标号的进程不可能逆向申请小标号的资源,这样就不会发生循环等待的现象;- 缺点:增加新的资源,可能需要重新分配标号;进程实际使用资源的顺序可能和标号顺序不一致,会导致资源浪费;必须按规定次序申请资源,用户编程麻烦;
六、线程安全的集合:
- 1、以
Concurrent开头
的集合类,可以支持多个线程并发写入访问,写入操作都是线程安全的,读取操作不必锁定,采用更复杂的算法,保证永不会锁住整个集合,因此在并发写入的时候有较好的性能;
ConcurrentLinkedQueue
实现多线程高效、无需等待的访问,不能使用null元素;
ConcurrentLinkedQueue
和ConcurrentLinkedHashMap
最好不要使用迭代器,因为迭代器可能不能反映出创建迭代器后所做的修改,但程序不会报出异常; - 2、以
CopyOnWrite
开头的集合类,采用复制底层数组的方式来实现写操作,读时无需加锁,对复制的新数组进行写操作,所以线程安全频繁的复制数组,性能不交叉,但是读操作没有加锁和阻塞就很快、很安全;