线程和进程的关系
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在现代面向线程的计算机结构之中进程是线程的容器。程序是指令,数据及其组织形式的描述,进程是程序的实体。线程是轻量级的进程,是程序执行的最小单位。使用多线程而不使用多进程,是应为线程之间的切换和调度成本远小于进程。
多线程执行一定快吗?
Cpu通过时间片分配算法循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是在切换之前会保存上一个任务的状态,以便下次切换回到这个任务可以再次加载这个任务的状态。所以任务从保存到再次加载的过程就是一次上下文切换,所以任务从保存到再次加载的过程就是一次上下文切换。
当线程很多的时候,进程的创建和上下文的切换都需要开销所以执行速度不一定快。
如何减少上下文的切换?
无锁并发(加锁会导致锁竞争),
使用适当的线程,尽量少;
协程:在单线程里实现多个任务调度
CAS算法:不加锁
多线程可以加快程序运行吗?
不能,并发是通过多个线程掠夺服务器带宽来实现资源的掠夺的,不能加快程序的运行,如果带宽存在限制,不仅速度不会快反而会更慢。需要根据资源的限制来调整并发的数量。
线程的创建:
实现Runnable或者继承Therad
实际上Runnable才是线程执行代码的宿主。
Thread本身就继承了Runnable
线程的状态:
public enum State {
NEW,//表示刚刚创建,但是还没有开始执行,等待start()方法调用
RUNNALE,//当线程执行的时候,,表示线程一切的资源都已经准备好了
BLOCKED,//当线程在执行的时候遇到了synchronized同步代码块,进入到blocked(阻塞)状态直到获得请求的锁
WAITING,//无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
TIME_WAITING,//等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
TERMINATED//当线程执行完毕之后
}
线程中断:
线程中断并不会使线程立即退出,而是给线程发送一个通知。告知目标线程需要退出,而目标线程接到通知后会如何处理,完全由目标线程决定。
三个方法:
public void interrupt() //中断当前线程(中断标记)
public boolean isInterrupted()// 判断是否被中断
public static boolean interrupted()// 判断是否被中断 ,并清除中断状态
public class Demo3 {
public static void main(String[] ars) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
if(Thread.currentThread().isInterrupted())
break;
System.out.println("222");
}
}
};
t1.start();
t1.sleep(400);
t1.interrupt();
}
}
虽然中断了线程但是如果没有做中断处理逻辑,即使被置上中断状态,这个中断也不会发生作用,只是做上一个标记。
Thread.sleep()
Thread.sleep()方法会让当前线程休眠一段时间,并抛出一个InterruptedException.这个异常不是运行时异常,必须捕获并处理它。如果当前线程正在休眠时被中断就会抛出这个异常。并且会清除中断标记。所以在必要的时候我们必须在异常的处理中重新设置中断标记。
public class Demo3 {
public static void main(String[] ars) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("中断");
break;
}
System.out.println("222");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
System.out.println("正在休眠");
//重新设置中断标记
Thread.currentThread().interrupt();
}
}
}
};
t1.start();
t1.sleep(400);
t1.interrupt();
}
}
等待(wait)通知(notify)
public final void notify()
public final void notifyAll()
public final void wait(long timeout)throws InterruptedException
如果一个线程调用了obj.wait() ,就会进入object对象等待队列,在这个队列中可能会有多个线程。因为系统中可能运行多个线程等待一个对象,当obj.notify()被调用时,他会从这个队列中随机选择一个线程,将其唤醒。这个选择完全是随机的。
notifyAll()会唤醒等待队列里的全部线程。
但是在调用wait()方法的时候必须要先获得object对象的监视器,执行之后会释放这个监视器。这样做的目的是使其他在等待object对象上的线程不至于因为T1线程的休眠而全部无法正常执行。而T2在执行notify()之前也必须获得objectd的监视器。接着唤醒一个等待的线程,假设为T1.T1被唤醒之后第一件事是重新获得object的监视器。只有顺利获得这个监视器才能顺利执行下面的代码。
Object.wait()和Thread.sleep()都会使线程等待若干时间。最主要的一个区别就是wait()会释放目标对象的锁,而sleep()不会释放任何资源。
挂起(suspend)和继续执行(resume)
挂起和执行时一对相反的操作,挂起起后导致线程暂停,不会释放任何的锁资源。
等待线程结束(join)和谦让(yield)
public final void join() throws InterruptedException 无限阻塞当前线程直到目标线程执行完毕
public final void join(long millis,int nanos)throws InterruptedException 给出等待的最大时间
package Test1;
public class Demo4 {
public volatile static int i = 0;
public static class AddThread extends Thread {
@Override
public void run() {
for (i = 0; i < 1000; i++) {
}
}
}
// 在主函数中如果没有at.join(),那么得到的i可能是一个非常小的数或者是0
// 因为AddThread还没有开始执行主线程就已经执行完毕了。
// 但是在加入at.join()表示主线程愿意等待AddThread执行完毕之后,再执行主线程。
public static void main(String[] args) throws InterruptedException {
AddThread at = new AddThread();
at.start();
at.join();
System.out.println(i);
}
}
public static void yield()
他会使当前线程放弃CPU的执行权,但是不表示当前线程不执行了。仍然会进行CPU的资源争夺。但是是随机的。
volatile关键字
当一个变量可能会不断被修改的时候使用volatile修饰。使所有的线程都能“看到”这个改动。
守护线程:
守护线程是一种特殊的线程,它在后台默默的执行一些系统性的服务,例如垃圾回收线程,JIT线程,与之对应的用户线程。当用户线程全部结束意味着这个程序的业务操作已经全部完成,守护线程要守护的对象已经不存在了。所以当一个java应用只有守护线程的时候,java虚拟机会自然退出。
设置守护线程:Thread.setDeamon(true),但是必须要在线程start()之前设置。
线程安全
Synchronize在jdk1.6前重量级锁
对于普通同步方法,锁是当前的实例对象
对于静态方法,锁是当前的类的class对象
对于同步的方法块,锁是Synchronized括号里的配置对象
指定对象加锁对象,必须是同一个对象
典型例子:脏读问题,设计程序一定要考虑业务的完整性
Synchronized的重入
不要使用字符串作为锁的对象,容易造成死锁
在使用锁的时候应当减小锁的粒度,哪里需要同步哪里加锁,使用同步代码块。
关于同步代码中出现异常,这个锁会被立即释放,我们应该根据具体的业务要求,判断是打印日志还是抛出运行时异常直接中断业务。
简单代码示例:
public class TraditionalLockExcepton implements Runnable {
private int i=0;
@Override
public void run() {
synchronized (this) {
while(true){
try {
i++;
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"i="+i);
if(i==10){
Integer.parseInt("a");
}
} catch (Exception e) {
System.out.println("Log info i = "+i);
//throw new RuntimeException();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new TraditionalLockExcepton());
t1.start();
t1.join();
}
}
Synchronized的原理和Jdk1.6后的优化
l 原理:
JVM是基于进入和退出的Monitors实现方法和同步代码块同步的,但是两者实现的细节不是一样,代码块同步是使用的monitorenter和monitorexit实现的,而方法是使用另一种方法,但是方法同样可以使用这两个指令来实现。
Monitorenter指令是在class文件编译后插入到同步代码块开始的位置,而monitorexit会插入到方法结束处和异常处。JVM会保证每个monitorenter都有与之对应的monitorexit与之配对。任何一个对象都有一个monitor对象与之关联,当且一个monitor被持有之后,他将处于锁定的状态,当执行到monitorenter指令的时候会尝试获得对象所对应的monitor对象,也就是获得锁,执行到monitorexit会释放这个对象。
l 优化
在jdk1.6之前synchronized是重量级锁,因为当线程多了之后,某一线程执完毕同步代码块后会释放锁,而其他的线程会抢夺锁,锁的竞争会导致cpu资源浪费,严重情况下甚至宕机。
为了优化获得锁和释放锁带来的性能消耗,在jdk1.6中共有四种锁的状态
无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。锁的状态只能升级不能降级。
偏向锁:在大多数情况下锁会由一个线程总是获得,当线程访问同步块,尝试获得锁的时候,会在对象头中存放锁偏向线程的id,下一次再访问的时候只需要简单的测试线程的id,而无需释放锁,只有当有其他线程竞争偏向锁的时候才会释放锁。
在jdk6和jdk7中偏向锁是默认开启的,但是他的开始是在程序启动几秒钟后激活,可以通过jvm参数来关闭延迟。
如果可以判断程序中的锁通常处于竞争状态我们可以通过jvm参数-XX:UseBiasedLock=false关闭偏向锁进入轻量级锁。
轻量级锁:
JDK并发包(JUC)
无锁并发的CAS操作,CAS操作使用无锁并发
CAS操作包含三个参数CAS(V,E,N),V表示要更新的变量,E表示预期值,N表示新值,只有当V等于N时才会设置值。
Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。
其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。
重入锁( java.util.concurrent.locks.ReentrantLock)
import java.util.concurrent.locks.ReentrantLock;
public class Demo6 implements Runnable {
// 重入锁
public static ReentrantLock lock = new ReentrantLock();
static int j = 0;
public static void main(String[] args) throws InterruptedException {
Demo6 tl = new Demo6();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t1.join();
t2.start();
t2.join();
System.out.println(j);
}
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
// 获取锁
lock.lock();
try {
j++;
} finally {
// 释放锁
lock.unlock();
}
}
}
}
观察上述代码可以发现,和synchronized相比,重入锁有着显式的操作过程,必须指定何时加锁,何时释放锁所以重入锁对逻辑的控制要远远好于synchronized。但是在退出临界区时必须释放锁。
重入锁允许一个线程连续两次获得同一把锁。如果不允许的话那么同一个线程在第二次获得锁时将会和自己产生死锁。注意的是加锁的次数和释放锁的次数必须相同,如果释放锁的次数过多将会获得一个IllegalMonitorStateException。
// 获取锁
lock.lock();
lock.lock();
try {
j++;
} finally {
// 释放锁
lock.unlock();
lock.unlock();
}
中断响应
对于synchornized来说,如果一个线程在等待锁。那么它要么获取锁继续执行要么保持继续等待。而重入锁提供了另外一种可能,就是线程可以中断。比如你和同学约好去打球,你等了半小时,突然接到一个电话他不能来了,那么你就只能回去了。假如一个线程正在等待锁那么他依然可以收到一个通知,被告知无需等待,可以停止工作了。这对于处理死锁有一定的帮助。
l import java.util.concurrent.locks.ReentrantLock;
ublic class Demo7 implements Runnable {
private int lock;
// 创建重入锁
private static ReentrantLock lock1 = new ReentrantLock();
private static ReentrantLock lock2 = new ReentrantLock();
public Demo7(int lock) {
this.lock = lock;
}
public static void main(String[] args) throws InterruptedException {
Demo7 r1 = new Demo7(1);
Demo7 r2 = new Demo7(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
Thread.sleep(2000);
//中断一个线程
t2.interrupt();
}
@Override
public void run() {
try {
if (lock == 1) {
// 如果当前线程未中断则获取锁
lock1.lockInterruptibly();
Thread.sleep(2000);
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
Thread.sleep(1000);
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 查询当前线程是否持有锁
if (lock1.isHeldByCurrentThread())
lock1.unlock();
if (lock2.isHeldByCurrentThread())
lock2.unlock();
System.out.println(Thread.currentThread().getId()+":线程退出");
}
}
}
在这里我们构建了一个死锁的程序,当主线程在睡眠的时候,线程t1先占用lock1,再占用lock2.而线程t2先占用lock2,再去请求lock1,导致死锁。最后我们中断了线程t2,t2放弃对lock1的请求,同时释放lock2
,t1请求lock2成功。执行完毕。
锁申请等待时限
除了等待外部通知以外,要避免死锁还有一种方法就是限时等待。通常一个线程无法拿到锁我们无法判断是死锁了还是产生了饥饿。但是如果我们给定一个等待的时间让线程来自动放弃是有意义的。使用tryLock()进行一次限时等待。
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
第一个参数是表示等待时常,第二个参数表示计时单位
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
l //限时等待锁
public class TimeLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
TimeLock tl = new TimeLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
}
@Override
public void run() {
//接收两个参数一个表示等待时常一个表示计时单位。
//在这里由于占用锁的线程会持有锁的时常长达6秒 而另一个线程无法在五秒的等带时间获取锁 请求锁失败
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
Thread.sleep(6000);
} else {
System.out.println("get lock failed");
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
if(lock.isHeldByCurrentThread())
lock.unlock();
}
}
}
public boolean tryLock()
无参的情况下,当前线程会尝试会尝试获得锁,如果锁未被其他线程占用,则锁会申请成功,返回true.如果锁被其他线程占用则当前线程不会等待立即返回false.这种模式不会引起线程等待,所以不会产生死锁。
import java.util.concurrent.locks.ReentrantLock;
public class TryLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public TryLock(int lock) {
this.lock = lock;
}
@Override
public void run() {
if (lock == 1) {
while (true) {
if (lock1.tryLock()) {
try {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
if (lock2.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + ":退出");
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
}
} else {
while (true) {
if (lock2.tryLock()) {
try {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
if (lock1.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + ":退出");
return;
} finally {
lock1.unlock();
}
}
} finally {
lock2.unlock();
}
}
}
}
}
public static void main(String[] args) {
TryLock r1 = new TryLock(1);
TryLock r2 = new TryLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
公平锁
公平锁的最大特点是不会产生饥饿现象,只要排队最终是可以得到资源的。如果使用synchronized关键字,那么产生的锁就是非公平的锁。重入锁允许我们对其进行公平性设置。
public ReentrantLock(boolean fair) 当参数设置为true时,表示锁是公平的。但是要实现一个公平锁系统必须维护一个有序队列,所以成本比较高,性能也相对比较底下。如果没有特别的需求,不需要使用公平锁。公平锁和非公平锁在线程调度上的表现也是非常不一样的。
l import java.util.concurrent.locks.ReentrantLock;
public class FairLock implements Runnable {
// 公平锁
public ReentrantLock lock = new ReentrantLock(true);
public static void main(String[] args) {
FairLock r = new FairLock();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
@Override
public void run() {
while (true) {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + ":获得锁");
} finally {
lock.unlock();
}
}
}
}
可以看出如果使用公平锁线程基本上交替获得锁的,几乎不会发生同一个线程连续多次获得锁的情况。从而公平性获得保证。如果使用非公平锁,情况会大不相同。
重入锁重要的api
1.lock() 获得锁 如果锁被占用则等待
2.lockInterruptibly() 获得锁,但是优先响应中断
3.boolean tryLock() 仅在调用时锁为空闲状态才获取该锁。如果锁可用,则获取锁,并立即返回值 true。如果锁不可用,则此方法将立即返回值 false。
4.tryLock(long time, TimeUnit unit) 在给定的时间内尝试获得锁
5.unlock() 释放锁
6. isHeldByCurrentThread() 查询当前线程是否保持此锁
重入锁的实现
1 原子状态
2等待队列
3阻塞原语park()和unpark() 挂起和恢复
重入锁搭档:Condition
1. CondItion
- void await() throws InterruptedException 使当前线程等待。同时释放当前锁,当其他线程中使用singal()或者singalAll()方法线程会重新获得锁继续执行。或当前线程被中断,也会跳出等待。和Object.wait()类似;
- awaitUninterruptibly() 和 await() 基本相同 但是不会在等待中响应中断
- void signal()唤醒一个线程 void signalAll() 唤醒所有线程 Object.notify();
l 测试代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReenterLockCondition implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
// 获得一个新的condition
public static Condition condition = lock.newCondition();
@Override
public void run() {
try {
lock.lock();
System.out.println("等待前》》》》");
condition.await();
System.out.println("Thread is going on");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLockCondition tl = new ReenterLockCondition();
Thread t1 = new Thread(tl);
t1.start();
Thread.sleep(5000);
// 通知t1继续执行
lock.lock();
condition.signal();
lock.unlock();
}
}
当调用Condition.await()后,线程释放这把锁,线程等待。当singal()调用的时候也要求t1先获得这把锁,当singal()方法调用后,系统会从当前Condition对象的等带队列中,唤醒一个线程,一旦线程被唤醒,他会重新尝试获得与之绑定的重入锁。获得成功之后就会继续执行。因此在singal()方法调用之后,一般需要释放锁,谦让给被唤醒的线程。如果最后没有lock.unlock().虽然唤醒了他t1但是由于它无法获得锁,就无法真正的执行。
2. 信号量(Semaphore)
广义上信号量是对锁的扩展,无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问临界区的资源,而信号量可以指定多个线程访问临界区资源。可以简单的理解为可以允许多个线程同时访问资源的锁
构造:
public Semaphore(int permits)
public Semaphore(int permits,boolean fair)
2.1 信号量主要的api
public void acquire() 获得一个准入的许可,若无法获得,则线程会等待,直到有线程释放一个许可或者当前线程被中断
public void acquireUninterruptibly() 和 acquire() 类似但是不会响应中断
public boolean tryAcquire()尝试获得一个许可,成功则返回true,失败则返回false不会进行等待 和tryLock()类似
public boolean tryAcquire(long timeout, TimeUnit unit)
public void release() 释放许可
3. 读写锁(ReadWriteLock)
不管是重入锁还是内部锁,当一个线程对资源进行读写的时候其他的线程都需要进行等待,但是当线程进行读操作的时候并不会对数据破坏所以这种等待显然是不合理的。所以在JDK1.5中提供了读写分离得锁,可以有效的减少锁的竞争以提升系统的性能。
读写锁访问约束
|
读 |
写 |
读 |
非阻塞 |
阻塞 |
写 |
阻塞 |
阻塞 |
l 测试代码
package Test1;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
// 重入锁
private static ReentrantLock lock = new ReentrantLock();
// 读写锁
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 读锁
private static Lock readLock = readWriteLock.readLock();
// 写锁
private static Lock writeLock = readWriteLock.writeLock();
private static int value;
// 读的方法
public void handleRead(Lock lock) throws InterruptedException {
try {
lock.lock();
Thread.sleep(2000);// 读的操作
System.out.println(value);
} finally {
lock.unlock();
}
}
// 写入数据
public void handleWrite(Lock lock, int index) throws InterruptedException {
try {
lock.lock();
Thread.sleep(1000);// 模拟写的操作
value = index;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
final ReadWriteLockDemo demo = new ReadWriteLockDemo();
// 读线程
Runnable readRunnable = new Runnable() {
@Override
public void run() {
try {
//demo.handleRead(readLock);
demo.handleRead(lock);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 写线程
Runnable writeRunnable = new Runnable() {
@Override
public void run() {
try {
//demo.handleWrite(writeLock, new Random().nextInt());
demo.handleWrite(lock, new Random().nextInt());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 18; i++) {
new Thread(readRunnable).start();
}
for (int i = 18; i < 20; i++) {
new Thread(writeRunnable).start();
}
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
}
}
上述代码中 模拟了都操作和写操作,让线程等待是一个耗时的操作,但是通过观察我们可以发现如果使用读写锁分离的操作线程很快就执行完了,这是因为只有在写入数据的时候是阻塞的(串行),而写入数据的线程只有两个,当写入数据完成之后是大量的读操作,而读操作是真正的并行操作是非阻塞的。所以会非常的快。
4. 倒计时器(CountDownLatch)
倒计时器通常用来控制线程的等待他可以让某一线程等待直到计时结束再开始执行。
构造:
Public CountDownLatch(int count) 传入计数器的个数
l 示例代码
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchDemo implements Runnable {
static final CountDownLatch cdt = new CountDownLatch(10);
static final CountDownLatchDemo demo = new CountDownLatchDemo();
@Override
public void run() {
try {
// 模拟检查任务
int task = new Random().nextInt(10) * 1000;
Thread.sleep(task);
System.out.println("检查完成" + task);
cdt.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
exec.submit(demo);
}
// 等待检查
cdt.await();
// 发射火箭
System.out.println("发射");
exec.shutdown();
}
}
CountDownLatch 和join类似但是CountDownLatch可以指定计时器的数量,举个简单的例子,当我们发射火箭的时候必须要先检查火箭各个部位的零件是否完好,然后通知总工程师,才可以发射火箭。上述代码计时器为10,表示要有10个线程完成任务,等待在CountDownLatch上的线程才能执行,第36行表示主线程要等代全部的检查任务执行后才能继续执行。第20行cdt.countDown()表示通知CountDownLatch有一个线程完成完成任务了,倒计时器可以减1,等10个任务全部执行完毕,主线程继续执行。
5循环栅栏(CyclicBarrier)
构造:public CyclicBarrier (int parties,Runnable barrierAction)
Parties 参与的线程数
barrierAction 计数完成后系统执行的动作
l 测试代码
import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
// 士兵
public static class Soldier implements Runnable {
private String soldier;
private final CyclicBarrier cyclic;
public Soldier(String soldier, CyclicBarrier cyclic) {
super();
this.soldier = soldier;
this.cyclic = cyclic;
}
@Override
public void run() {
try {
// 等待所有士兵到齐
cyclic.await();
doWork(soldier);
// 等待所有士兵完成任务
cyclic.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public static void doWork(String soldier) {
try {
Thread.sleep(Math.abs(new Random().nextInt() % 10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(soldier + ":任务完成");
}
public static class BarrierRun implements Runnable {
boolean flag;
int N;
public BarrierRun(boolean flag, int n) {
super();
this.flag = flag;
this.N = n;
}
@Override
public void run() {
if (flag) {
System.out.println("司令:[士兵" + N + "个" + "任务完毕]");
} else {
System.out.println("司令:[士兵" + N + "个" + "集合完毕]");
flag = true;
}
}
}
public static void main(String[] args) {
// 士兵的个数
final int N = 10;
Thread[] allSoldier = new Thread[10];
boolean flag = false;
// 循环栅栏
CyclicBarrier cyclic = new CyclicBarrier(N, new BarrierRun(flag, N));
System.out.println("集合队伍!");
for (int i = 0; i < N; i++) {
System.out.println("士兵" + i + "报道");
allSoldier[i] = new Thread(new Soldier("士兵" + i, cyclic));
allSoldier[i].start();
}
}
}
观察以上代码司令发出命令第一步十个士兵前来报道,报道完成之后,第二步执行任务。代码第77行创建一个循环栅栏,计数器为10然后在代码第23行执行第一个cyclic.await();所有士兵报道完成(第一次计数完成)后进入BarrierRun 输出,然后等待所有的士兵完成任务(第二次计数),再进入BarrierRun 完成输出
CountDownLatch和CyclicBarrier的区别图解
检查任务countDownLatch.await countDownLatch.countDown();
主线程
主线程
士兵 CyclicBarrier.await() CyclicBarrier.await()
barrierAction barrierAction
第一次计数 集合完毕 第二次计数
5. 线程阻塞工具(LockSipport)
线程池
需要工作的时候从线程池中取得线程,工作的时候将线程放回到线程池里。避免频繁创建线程和销毁线程的开销。
Executors类是线程池工厂,通过Executors可以获得一个指定功能的线程池。
NewFiledThreadPool,创建固定数量的线程池,当新的任务提交的时候,线程池中如果有空闲线程则立即执行,没有线程空闲放到等待队列中等代。
NewSingleThreadExecutor,创建只有一个线程的线程池。
NewCachedThreadPool,创建不固定数量的线程池
NewScheduledThreadPool 任务调度的线程池,类似于TimerTask
核心线程池的内部实现
参见博客:
http://blog.csdn.net/qq_25235807/article/details/73435419
锁的优化和并发类容器
- 减少锁持有的时间,只在必要的时候对代码加锁,例如非空的时候再加锁,未编译时加锁。没有必要的时候不加锁。
- 减小锁的粒度,例如并发类容器ConcurrentHashMap
- 读写分离锁代替独占锁
- 锁的粗化
并发类容器
ConCurrentHashMap:典型的减小锁的粒度的实现,他的内部进一步细分了若干个小的hashMap,称之为段(SEGMENT),默认情况下将一个ConcurrentHashMap分为16个段,当在ConCurrentHashMap添加数据的时候会先根据hashcode获取对应的段,然后获得这个段,对这个段加锁,然后完成put操作。
减小锁的粒度之后引入了一个新的问题,就是在获取全局信息的时候,消耗的资源会比较多,例如在获得ConCurrentHashMap的size()方法,返回有效表项数量,就要获得所有的子段锁,所以只有当类似于获取全局信息不频繁的时候才会真正提高系统的吞吐量。
对于读操作却完全不需要加锁参见博客:
http://www.iteye.com/topic/344876
实现了读写分离
CopyOnWriteArrayList:(适用于读多写少,且数据量不是很大的情况)
读写锁,读锁和读锁确实不冲突,但是读的操作会受到写的影响。CopyOnWriteArrayList的读操作是完全不加锁的,写入也不会阻塞读的操作。在写入的时候会进行一次自我复制,对List修改的时候不修改原来的内容而是将修改的内容写入原来的副本,这个List被Volatile关键字修饰,使得写完之后能被立即看见,替换原来的List。
ConcurrentLinkedQueue:高效的无锁并发队列,不允许存放null
ArrayBlockingQueue:(有界队列内部维护的是一个定长数组)
Put() 和take()才是体现阻塞的核心
LinkedblockingQueue:(无界队列内部维护的是双向链表)
因为LinkedList的特性,take()在头部而put在尾部,所以两者不会冲突
因此在内部采用读写分离的两个锁。
SynchronousQueue:(直接提交队列,不允许存放任务,直接提交给线程执行)
PrioirtyBlockingQueue:(带有优先级的队列,任务必须实现Compareable接口)
DelayQueue:带有延时的队列
一般有如下的应用:
a) 关闭空闲连接。服务器中,有很多客户端的连接,空闲一段时间之后需要关闭之。
b) 缓存。缓存中的对象,超过了空闲时间,需要从缓存中移出。
c) 任务超时处理。在网络协议滑动窗口请求应答式交互时,处理超时未响应的请求。
小案例:网吧上网:
http://blog.csdn.net/qq_25235807/article/details/73292759
典型模式
Future模式
http://blog.csdn.net/qq_25235807/article/details/73308450
Master-Worker模式
简单的并发计算模式
生产者—消费者模式