- synchronized
synchronized可以实现方法和代码块的同步。但是当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。这是一个致命的问题。(synchronized(“字符串常量”)这种锁经常会出现莫名其妙的问题)。 - 原子类AtomicInteger
一个提供原子操作的Integer的类,还有其他的原子类这里就不一一举出了。在Java语言中,++i和i++操作并不是线程安全的,如果使用synchronized并发执行速度比较慢,而AtomicInteger是使用了CAS算法也就是无锁,这个可以大大的提升性能。下面代码是无锁的一个实现
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the updated value
*/
public final int addAndGet(int delta) {
for (;;) {
int current = get();
int next = current + delta;
if (compareAndSet(current, next))
return next;
}
}
- volatile可见性
用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的,并非原子性的,具体怎么使用最好参考网上的资料,不然会出现一些问题。 - double check Singleton二次检查锁定
一般的单例子在多线程使用的时候会出错,原因很明显这里就不多讲了。下面是二次检查锁定的代码
public class DubbleSingleton {
private static DubbleSingleton ds;
public static DubbleSingleton getDs(){
if(ds == null){
try {
//模拟初始化对象的准备时间...
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (DubbleSingleton.class) {
if(ds == null){
ds = new DubbleSingleton();
}
}
}
return ds;
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(DubbleSingleton.getDs().hashCode());
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(DubbleSingleton.getDs().hashCode());
}
},"t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(DubbleSingleton.getDs().hashCode());
}
},"t3");
t1.start();
t2.start();
t3.start();
}
}
这里面最有疑问的应该是第二次检查了,原因是因为但线程1在锁定进入实例化的时候,线程2和线程3也通过了第一次检查,当线程1实例化后线程2和线程3还会进入同步块进行实例化对象,因此需要在同步代码块中进行判断是否有实例化对象,防止再次实例化对象,如果还是不清楚把上面代码进行debug一步步运行就知道了。
- 静态内部类单例
一般在多线程单例中使用较多的是用内部类单例来实现, 由于内部静态类只会被加载一次,所以这个是线程安全的。
public class Singletion {
private static class InnerSingletion {
private static Singletion single = new Singletion();
}
public static Singletion getInstance(){
return InnerSingletion.single;
}
}
wait方法与notfiy方法、notifyAll方法
wait方法就会使持有该对象的线程把该对象的控制权交出去也就是把锁释放,然后处于等待状态。
notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行,但是不释放锁。
notifyAll方法跟notify方法差不多,不过他是通知所有的线程的。
注意:1.当使用 wait 和 notify 的时候 , 一定要配合着synchronized关键字去使用;
2.假设线程1在代码中通知正在等待的线程2,但是线程1的同步代码块还没有全部执行完,这时候线程2是不会接收到线程1的通知的,只有当线程1的同步代码块执行完后才会去通知线程2可以执行了,也就是说notfiy不是实时的,如果要求实时的化可以使用CountDownLatch类去实现。CountDownLatch
一个同步辅助类,用给定的计数初始化 CountDownLatch。调用了 countDown() 方法,就减少一次当前计数,在到达零之前,await 方法会一直受阻塞。在到达零的时候,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。ThreadLocal
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路,每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。CopyOnWriteArrayList
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。ConcurrentHashMap
理解这个的时候,先比较下hashtable和hashmap的区别,hashtable是做了同步的,hashmap未考虑同步,也就是非线程安全的,但是hashmap在单线程情况下效率较高。ConcurrentHashMap保证了线程安全的情况下速度也较hashtable提高了不少。ConcurrentHashMap引入了段的概念,他把数据分成了16个段要进行锁定,也就是锁的方式是稍微细粒度的,诸如get,put,remove等常用操作只锁当前需要用到的桶。ConcurrentHashMap的读取并发,因为在读取的大多数时候都没有用到锁定,所以读取操作几乎是完全的并发操作,而写操作锁定的粒度又非常细,比起之前又更加快速(这一点在桶更多时表现得更明显些)。只有在求size等操作时才需要锁定整个表。非阻塞队列
非阻塞队列基于链接节点的、无界的、线程安全。此队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部 是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部,队列检索操作从队列头部获得元素。当许多线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。此队列不允许 null 元素。-阻塞队列
实现阻塞队列的方法:
阻塞队列(BlockingQueue),阻塞队列是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
DelayQueue
DelayQueue主要用于放置实现了Delay接口的对象,其中的对象只能在其时刻到期时才能从队列中取走。这种队列是有序的,即队头的延迟到期时间最短。如果没有任何延迟到期,那么就不会有任何头元素,并且poll()将返回null。PriorityBlockingQueue
PriorityBlockingQueue里面存储的对象必须是实现Comparable接口。队列通过这个接口的compareTo方法确定对象的priority。规则是:当前和其他对象比较,如果compareTo方法返回负数,那么在队列里面的优先级就比较高。Future模式
在Java中,创建线程一般有两种方式,一种是继承Thread类,一种是实现Runnable接口。这两种方式的缺点是在线程任务执行结束后,无法获取执行结果,所以一般采用共享变量或共享存储区以及线程通信的方式实现获得任务结果的目的。不过,Java中,提供了使用Callable和Future来实现获取任务结果的操作。Callable用来执行任务,产生结果,而Future用来获得结果。下面是例子
public class UseFuture implements Callable<String>{
private String para;
public UseFuture(String para){
this.para = para;
}
/**
* 这里是真实的业务逻辑,其执行可能很慢
*/
@Override
public String call() throws Exception {
//模拟执行耗时
Thread.sleep(5000);
String result = this.para + "处理完成";
return result;
}
//主控制函数
public static void main(String[] args) throws Exception {
String queryStr = "query";
//构造FutureTask,并且传入需要真正进行业务逻辑处理的类,该类一定是实现了Callable接口的类
FutureTask<String> future = new FutureTask<String>(new UseFuture(queryStr));
FutureTask<String> future2 = new FutureTask<String>(new UseFuture(queryStr));
//创建一个固定线程的线程池且线程数为1,
ExecutorService executor = Executors.newFixedThreadPool(2);
//这里提交任务future,则开启线程执行RealData的call()方法执行
//submit和execute的区别: 第一点是submit可以传入实现Callable接口的实例对象, 第二点是submit方法有返回值
Future f1 = executor.submit(future); //单独启动一个线程去执行的
Future f2 = executor.submit(future2);
System.out.println("请求完毕");
try {
//这里可以做额外的数据操作,也就是主程序执行其他业务逻辑
System.out.println("处理实际的业务逻辑...");
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
//调用获取数据方法,如果call()方法没有执行完成,则依然会进行等待
System.out.println("数据:" + future.get());
System.out.println("数据:" + future2.get());
executor.shutdown();
}
}
- Master-Worker模式
Master-Worker模式是常用的并行模式之一,它的核心思想是:系统由两类进程协同工作,即Master进程和Worker进程,Master负责接收和分配任务,Wroker负责处理子任务。当各个Worker进程将子任务处理完成后,将结果返回给Master进程,由Master进程进行汇总,从而得到最终的结果,其具体处理过程如下图所示。
Master进程为主要进程,它维护一个Worker进程队列、子任务队列和子结果集。Worker进程队列中的Worker进程不停从任务队列中提取要处理的子任务,并将结果写入结果集。 - Executors线程池
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
在这里面newScheduledThreadPool 比较特殊点,如果使用有界队列来缓存请求,那超出最大请求数量就会抛出异常(这里的异常你可以自定义去处理,也可以用Java自带的,最好还是自己去处理),如果使用无界队列来缓存请求,那第二个参数最大请求数就会失效。如果项目中有使用到spring的话,可以使用更简单的Spring的@Scheduled实现定时任务。 - CyclicBarrier
CyclicBarrier和CountDownLatch一样,都是关于线程的计数器,区别是CountDownLatch操作的一个线程,而CyclicBarrier操作的是多个线程。
public class UseCyclicBarrier {
static class Runner implements Runnable {
private CyclicBarrier barrier;
private String name;
public Runner(CyclicBarrier barrier, String name) {
this.barrier = barrier;
this.name = name;
}
@Override
public void run() {
try {
Thread.sleep(1000 * (new Random()).nextInt(5));
System.out.println(name + " 准备OK.");
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(name + " Go!!");
}
}
public static void main(String[] args) throws IOException, InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(3); // 3
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.submit(new Thread(new Runner(barrier, "zhangsan")));
executor.submit(new Thread(new Runner(barrier, "lisi")));
executor.submit(new Thread(new Runner(barrier, "wangwu")));
executor.shutdown();
}
}
从上面的代码可以看出,当线程池Executors启动的线程达到CyclicBarrier初始化时给的数字,那所有的线程都将立即执行,不然程序会一直阻塞在那里。
- Semaphore
Semaphore当前在多线程环境下被扩放使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。Java 并发库 的Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
public class UseSemaphore {
public static void main(String[] args) {
// 线程池
ExecutorService exec = Executors.newCachedThreadPool();
// 只能5个线程同时访问
final Semaphore semp = new Semaphore(5);
// 模拟20个客户端访问
for (int index = 0; index < 20; index++) {
final int NO = index;
Runnable run = new Runnable() {
public void run() {
try {
// 获取许可
semp.acquire();
System.out.println("Accessing: " + NO);
//模拟实际业务逻辑
Thread.sleep((long) (Math.random() * 10000));
// 访问完后,释放
semp.release();
} catch (InterruptedException e) {
}
}
};
exec.execute(run);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println(semp.getQueueLength());
// 退出线程池
exec.shutdown();
}
}
- ReentrantLock
在功能上面和synchronized差不多都是实现锁,不同的是ReentrantLock相比之下比synchronized更灵活些,使用上最主要是因为加了Condition,下面是代码,需要注意在使用后在finally代码里面要把锁给释放掉,不然有可能会永远锁定。
public class UseReentrantLock {
private Lock lock = new ReentrantLock();
public void method1(){
try {
lock.lock();
System.out.println("当前线程:" + Thread.currentThread().getName() + "进入method1..");
Thread.sleep(1000);
System.out.println("当前线程:" + Thread.currentThread().getName() + "退出method1..");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void method2(){
try {
lock.lock();
System.out.println("当前线程:" + Thread.currentThread().getName() + "进入method2..");
Thread.sleep(2000);
System.out.println("当前线程:" + Thread.currentThread().getName() + "退出method2..");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final UseReentrantLock ur = new UseReentrantLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
ur.method1();
ur.method2();
}
}, "t1");
t1.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println(ur.lock.getQueueLength());
}
}
- ReentrantReadWriteLock
ReentrantReadWriteLock读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!
public class UseReentrantReadWriteLock {
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private ReadLock readLock = rwLock.readLock();
private WriteLock writeLock = rwLock.writeLock();
public void read(){
try {
readLock.lock();
System.out.println("当前线程:" + Thread.currentThread().getName() + "进入...");
Thread.sleep(3000);
System.out.println("当前线程:" + Thread.currentThread().getName() + "退出...");
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
public void write(){
try {
writeLock.lock();
System.out.println("当前线程:" + Thread.currentThread().getName() + "进入...");
Thread.sleep(3000);
System.out.println("当前线程:" + Thread.currentThread().getName() + "退出...");
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
final UseReentrantReadWriteLock urrw = new UseReentrantReadWriteLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
urrw.read();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
urrw.read();
}
}, "t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
urrw.write();
}
}, "t3");
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
urrw.write();
}
}, "t4");
// t1.start();
// t2.start();
// t1.start(); // R
// t3.start(); // W
t3.start();
t4.start();
}
}