并发编程

线程安全

线程安全概念 : 当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的.

synchronized : 可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区".

总结 :

当多个 线程访问myThread的run方法时,以排队的方式进行处理(这里排队是按照CPU分配的先后顺序而定的),一个线程想要执行synchronized修饰的方法里的代码,首先

是尝试获得锁,如果拿到锁,执行synchronized方法体的内容;拿不到锁,这个线程就会不断的尝试获得这把锁,直到拿到为止,而且是多个线程同时去竞争这把锁.(也就是会有锁竞争的问题)

多个线程多个锁 : 多个线程,每个线程都可以拿到自己指定的锁,分别获得锁之后,执行synchronized方法体的内容.

总结 :

关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当做锁,所以示例代码中哪个线程先执行synchronized关键字的方法,那个线程就持有该方法所属对象的

锁(Lock),两个对象,线程获得的就是两个不同的锁,他们互不影响.

有一种情况则是相同的锁,既在静态方法上加synchronized关键字,表示锁定.class类,类一级别的锁(独占.class类)

对象锁的同步和异步

同步 : synchronized

同步的概念就是共享,我们要牢牢记住"共享"这两个字,如果不是共享的资源,就没有必要进行同步.

异步 : asynchronized

异步的概念就是独立,相互之间不受到任何制约.就好像我们学校http的时候,在页面发起的Ajax请求,我们还可以继续浏览或操作页面的内容,二者之间没有任何关系.

同步的目的就是为了线程安全,其实对于线程安全来说,需要满足两个特性 : 原子性(同步),可见性.

总结 :

A线程先持有object对象的Lock锁, B线程如果在这个时候调用对象中的同步(synchronized)方法则需等待, 也就是同步;

A线程先持有object对象的Lock锁, B线程可以以异步的方法调用对象中的非synchronized修饰的方法.

脏读

对于对象的同步和异步的方法,我们在设计自己的程序的时候,一定要考虑问题的整体,不然就会出现数据不一致的错误,很经典的错误就是脏读(dirtyread)

总结 :

在我们对一个对象的方法加锁的时候,需要考虑业务的整体性,既为setValue/getValue方法同时加锁synchronized同步关键字,保证业务(service)的原子性,不然会出现

业务的错误(也从侧面保证业务的一致性)

Oracle数据库一致性读的概念 :

undo主要有三大作用 : 提供一致性读(Consistent Read), 回滚事务(Rollback Transaction)以及实例恢复(Instance Recovery).

线程之间通信

线程通信概念 : 线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体,线程间的通信就成为整体的必用方式之一.当线程存在通信指挥,系统间

的交互性会更强大,在提高CPU利用率的同时还会使开发人员对线程任务在处理的过程中进行有效的把控与监督.

使用wait / notify 方法实现线程间的通信.(注意这两个方法都是object的类的方法,换句话说java为所有的对象都提供了这两个方法)

1 : wait和notify必须配合synchronized关键字使用;

2 : wait方法释放锁,notify方法不释放锁.

在Java中可以用wait,notify和notifyAll来实现线程间的通信.线程在运行的时候,如果发现某些条件没有被满足,可以调用wait方法暂停自己的执行,并且放弃已经获得的锁,

然后进入等待状态.当该线程被其他线程唤醒并获得锁后,可以沿着之前暂停的地方继续向后执行,而不是再次从同步代码块开始的地方开始执行.但是需要注意的一点是,对线程

等待的条件的判断要使用while而不是if来进行判断.这样在线程被唤醒后,会再次判断条件是否正真满足.

案例 :

package mythread.mythread;

import java.util.ArrayList;

import java.util.List;

public class ListAdd1 {

private volatile static List list = new ArrayList();

public void add() {

list.add("bjsxt");

}

public int size() {

return list.size();

}

public static void main(String[] args) {

final ListAdd1 list1 = new ListAdd1();

Thread t1 = new Thread(new Runnable() {

@Override

public void run() {

try {

for (int i = 0; i < 10; i++) {

list1.add();

System.out.println("当前线程 : " + Thread.currentThread().getName() + "添加了一个元素..");

Thread.sleep(500);

}

} catch (Exception e) {

e.printStackTrace();

}

}

}, "t1");

Thread t2 = new Thread(new Runnable() {

@Override

public void run() {

while (true) {

if (list1.size() == 5) {

System.out.println("当前线程收到通知 : " + Thread.currentThread().getName() + "list size = 5线程停止");

throw new RuntimeException();

}

}

}

}, "t2");

t1.start();

try {

Thread.sleep(5);

} catch (InterruptedException e) {

e.printStackTrace();

}

t2.start();

}

}

package mythread.mythread;

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.CountDownLatch;

/**

* wait notfiy 方法,wait释放锁,notfiy不释放锁

*/

public class ListAdd2 {

private volatile static List list = new ArrayList<>();

public void add() {

list.add("bjsxt");

}

public int size() {

return list.size();

}

public static void main(String[] args) {

final ListAdd2 list2 = new ListAdd2();

// 1.实例化出来一个lock

// 2.当使用wait和notify的时候,一定要配合着synchronized关键字去使用

// final Object lock = new Object();

final CountDownLatch countDownLatch = new CountDownLatch(1);

Thread t1 = new Thread(new Runnable() {

@Override

public void run() {

try {

///synchronized (lock) {

for (int i = 0; i < 10; i++) {

list2.add();

System.out.println("当前线程 : " + Thread.currentThread().getName() + "添加了一个元素..");

Thread.sleep(500);

if (list2.size() == 5) {

System.out.println("已结发出通知 ..");

countDownLatch.countDown();

//lock.notify();

}

}

  // }

} catch (Exception e) {

e.printStackTrace();

}

}

}, "t1");

Thread t2 = new Thread(new Runnable() {

@Override

public void run() {

//synchronized (lock) {

if (list2.size() != 5) {

try {

System.out.println("t2进入...");

Thread.sleep(3000);

//lock.wait();

countDownLatch.await();

} catch (Exception e) {

e.printStackTrace();

}

}

System.out.println("当前线程 : " + Thread.currentThread().getName() + "收到通知线程停止..");

throw new RuntimeException();

}

//}

}, "t2");

t2.start();

t1.start();

}

}

使用wait/notify模拟Queue

BlockingQueue : 顾名思义,首先它是一个队列,并且支持阻塞的机制,阻塞的放入和得到数据.我们要实现LinkedBlockingQueue下面两个简单的方法put和take.

put(anObject) : 把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断,直到BlockingQueue里面有空间再继续.

take : 取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入.

例子 :

package mythread.mythread;

import java.util.LinkedList;

import java.util.concurrent.atomic.AtomicInteger;

public class MyQueue {

// 1.需要一个承装元素的集合

private LinkedList<Object> list = new LinkedList<Object>();

// 2需要一个计数器

private AtomicInteger count = new AtomicInteger(0);

// 3需要制定上限和下限

private final int minSize = 0;

private final int maxSize;

// 4构造方法

public MyQueue(int maxSize) {

this.maxSize = maxSize;

}

// 5初始化一个对象,用于加锁

private final Object lock = new Object();

// put(anObject) : 把anObject加到BlockQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断,直到BlockingQueue里面有空间再继续.

public void put(Object obj) {

synchronized (lock) {

while (count.get() == this.maxSize) {

try {

lock.wait();

} catch (Exception e) {

e.printStackTrace();

}

}

//1 加入元素

list.add(obj);

//2计数器累加

count.incrementAndGet();

//3通知另外一个线程(唤醒)

lock.notify();

System.out.println("新加入的元素为 : " + obj);

}

}

//take : 取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入.

public Object take() {

Object ret = null;

synchronized (lock) {

while (count.get() == this.minSize) {

try {

lock.wait();

} catch (Exception e) {

e.printStackTrace();

}

}

//1做移除元素操作

ret = list.removeFirst();

//2计数器递减

count.decrementAndGet();

//3唤醒另外一个线程

lock.notify();

}

return ret;

}

public static void main(String[] args) {

final MyQueue mq = new MyQueue(5);

mq.put("a");

mq.put("b");

mq.put("c");

mq.put("d");

mq.put("e");

System.out.println("当前容器的长度 : " + mq.getSize());

Thread t1 = new Thread(new Runnable() {

@Override

public void run() {

mq.put("f");

mq.put("g");

}

}, "t1");

t1.start();

Thread t2 = new Thread(new Runnable() {

@Override

public void run() {

Object o1 = mq.take();

System.out.println("移除的元素为 : " + o1);

Object o2 = mq.take();

System.out.println("移除的元素为 : " + o2);

}

}, "t2");

try {

Thread.sleep(2000);

} catch (InterruptedException e) {

e.printStackTrace();

}

t2.start();

}

public int getSize() {

return list.size();

}

}

ThreadLocal概念 : 线程局部变量,是一种多线程间并发访问变量的解决方案.与其synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而使用以空间换时间的手段,

为每个线程提供变量的独立副本,以保障线程安全.从性能上说,ThreadLocal不具有绝对的优势,在并发不是很高的时候,加锁的性能会更好,但作为一套与锁完成无关的

线程安全解决方案,在高并发量或者竞争激烈的场景,使用ThreadLocal可以在一定程度上减少锁竞争.

例子 :

package mythread.mythread;

public class ConnThreadLocal {

public static ThreadLocal<String> th = new ThreadLocal<String>();

public void setTh(String value) {

th.set(value);

}

public void getTh() {

System.out.println(Thread.currentThread().getName() + ":" + this.th.get());

}

public static void main(String[] args) throws Exception{

final ConnThreadLocal ct = new ConnThreadLocal();

Thread t1 = new Thread(new Runnable() {

@Override

public void run() {

ct.setTh("张三");

ct.getTh();

}

}, "t1");

Thread t2 = new Thread(new Runnable() {

@Override

public void run() {

try {

Thread.sleep(1000);

ct.getTh();

} catch (Exception e) {

e.printStackTrace();

}

}

}, "t2");

t1.start();

t2.start();

}

}

单例&多线程

单例模式,最常见的就是饥饿模式,和懒汉模式,一个直接实例化对象,一个在调用方法时进行实例化对象.在多线程模式中,考虑到性能和线程安全问题,我们一般选择下面

两者经典的单例模式,在性能提高的同时,又保证了线程安全.

dubble check instance

static inner class

例子 :

package mythread.mythread;

/**

* 静态内部类实现单例模式

*/

public class Singletion {

private static class InnerSingletion {

private static Singletion singe = new Singletion();

}

public static Singletion getInstance() {

return InnerSingletion.singe;

}

}

同步类容器

同步类容器都是线程安全的,但在某些场景下可能需要加锁来保护复合操作.复合类操作如 : 迭代(反复访问元素,遍历完容器中所有的元素),跳转(根据指定的顺序找到当前

的下一个元素),以及条件运算.这些复合操作在多线程并发的修改容器时,可能会表现出意外的行为,最经典的便是ConcurrentModificationException,原因是当容器迭代的

过程中,被并发的修改了内容,这是由于早期迭代器设计的时候并没有考虑并发修改的问题.

同步类容器 : 如古老的Vector,HashTable.这些容器的同步功能其实都是由JDK的Collections.synchronized***等工厂方法区创建实现的.其底层的机制无非就是用传统的

synchronized关键字对每个公用的方法都进行同步,使得每次只能有一个线程访问容器的状态.这很明显不满足我们今天互联网时代高并发的需求,在保证线程安全的同时,

也必须要有足够好的性能.

并发类容器

jdk5.0以后提供了多种并发类容器来替代同步类容器从而改善性能.同步类容器的状态都是串行化的.他们虽然实现了线程安全,但是严重降低了并发性,在多线程环境时,

严重降低了应用程序的吞吐量.

并发类容器时专门针对并发设计的,使用ConcurrentHashMap来代替给予散列的传统的HashTable,而且在ConcurrnetHashMap中,添加了一些常见符复合操作的支持.以及使用

了CopyOnWriteArrayList代替Voctor,并发的CopyonWriteArraySet,以及并发的Queue,ConcurrentLinkedQueue和LinkedBlockingQueue,前者是高性能的队列,后者是以阻塞

形式的队列,具体实现Queue还有很多,例如ArrayBlockingQueue,PriorityBlockingQueue,SynchronousQueue等.

ConcurrentMap

ConcurrentMap接口下有两个重要的实现 :

ConcurrentHashMap

ConcurrentSkipListMap(支持并发排序功能,弥补ConcurrentHashMap)

ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的HashTable,他们有自己的锁.只要多个修改操作发生在不同的段上,他们就可以

并发进行.把一个整体分成了16个段(Segment).也就是最高支持16个线程的并发修改操作.这也是在多线程场景时减少锁的粒度从而降低锁竞争的一种方案.并且代码中大

多共享变量使用volatile关键字声明,目的是第一时间获取修改的内容,性能非常好.

Copy-On-Write容器

Copy-On-Write简称COW,是一种用于程序设计中的优化策略.

JDK里的COW容器有两种 : CopyOnWriteArrayList和CopyOnWriteArraySet,COW容器非常有用,可以在非常多的并发场景中使用到.

什么是CopyOnWrite容器?

CopyOnWrite容器既写时复制的容器.通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后

新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器.这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加

任何元素.所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器.

并发Queue

在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能队列,一个是以BlockingQueue接口为代表的阻塞队列,无论哪种继承自Queue;

ConcurrentLinkedQueue

ConcurrentLinkedQueue : 是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue.它

是一个基于链接节点的无界线程安全队列.该队列的元素遵循先进先出的原则.头是最先加入的,尾是最近加入的,该队列不容许null元素.

ConcurrentLinkedQueue重要方法 :

add()和offer()都是加入元素的方法(在ConcurrentLinkedQueue中,这两个方法没有任何区别)

poll()和peek()都是取头元素节点,区别在于前者会删除元素,后者不会.

BlockingQueue接口

ArrayBlockingQueue : 基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,其内部没实现读写分离,也就是意味着

生产和消费不能完全并行,长度是需要定义的,可以指定先进先出或者先进后出,也叫有界队列,在很多场合非常适合使用.

LinkedBlockingQueue : 基于链表的阻塞队列,同ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),LinkedBlockingQueue之所以能够

高效的处理并发数据,是因为其内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作的完全并行运行.他是一个无界队列.

SynchronousQueue : 一种没有缓冲的队列,生产者产生的数据直接会被消费者获取并消费.

PriorityBlockingQueue : 基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定,也就是说传入队列的对象必须实现Comparable接口),在实现

PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁,他也是一个无界的队列.

DelayQueue : 带有延迟时间的Queue,其中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素.DelayQueue中的元素必须实现Delayed接口,DelayQueue是

一个没有大小限制的队列,应用场景很多,比如对缓存超时的数据进行移除,任务超时处理,空闲链接的关闭等等.

Master-Worker模式

Master-Worker模式是常用的并行计算模式.它的核心思想是系统由两类进程协作工作 : Master进程和Workder进程.Master负责接收和分配任务,Worker负责处理子任务.

当各个Worker子进程处理完成后,会将结果返回给Master,由Master做归纳和总结.其好处是能将一个大任务分解成若干个小任务,并行执行,从而提高系统的吞吐量.

生产者-消费者

生产者和消费者也是一个多线程模式,我们在实际开发中应用非常广泛的思想理念.在生产-消费模式中 : 通常由两类线程,即若干个生产者的线程和若干个消费者的线程.

生产者线程负责提交用户请求,消费者线程则负责具体处理生产者提交的任务,在生产者和消费者之间通过共享内存缓存区进行通信.

Executor框架

创建线程池方法 :

newFixedThreadPool()方法,该方法返回一个固定数量的线程池,该方法的线程数始终不变,当有一个任务提交时,若线程池中空闲,则立即执行,若没有,则会被暂缓在

一个任务队列中等待有空闲的线程去执行.

newSingleThreadExecutor()方法,创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中.

newCachedThreadPool()方法,返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若有任务,则创建线程,若无任务则不创建线程.如果 没有任务则

线程在60s后自动回收(空闲时间60s).

newScheduledThreadPool()方法,该方法返回一个SchededExecutorService对象,但该线程池可以指定线程的数量.

自定义线程池使用详细

这个构造方法对于队列是什么类型的比较关键 :

在使用有界队列时,若有新的任务需要执行,如果线程池实际线程数小于corePoolSize,则优先创建线程,若大于corePoolSize,则会将任务加入队列,若队列已满,则在

总线程数不大于maximumPoolSize的前提下,创建新的线程,若线程数大于maximumPoolSize,则执行拒绝策略.或其他自定义方式.

无界的任务队列时,LinkedBlockingQueue.与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况.当有新任务到来,系统的线程数小于

corePoolSize时,则新建线程执行任务.当达到corePoolSize后,就不会继续增加.若后续任有新的任务加入,而有没有空闲的线程资源,则任务直接进入队列等待.若任务创建

和处理的速度差异很大,无界队列会保持快速 增长,直到耗尽系统内存.

JDK拒绝策略 :

AbortPolicy : 直接抛出异常阻止系统正常工作;

CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务.

DiscardOldestPolicy : 丢弃最老的一个请求,尝试再次提交当前任务.

DiscardPolicy : 丢弃无法处理的任务,不给予任何处理.

如果需要自定义拒绝策略可以实现RejectedExecutionHandler接口.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容

  • layout: posttitle: 《Java并发编程的艺术》笔记categories: Javaexcerpt...
    xiaogmail阅读 5,802评论 1 19
  • 1、线程安全与锁 线程安全的本质,在于 存在了共享的可变状态 status, 在多线程共同操作状态变量时,当计算的...
    轩居晨风阅读 339评论 1 1
  • 每一个想学习Java多线程的人,手里至少有这本书或者至少要看这本书。强烈建议大家多看几遍。 代码中比较容易出现bu...
    玥玥籽阅读 931评论 0 0
  • 上周的面试中,被问及了几个并发开发的问题,自己回答的都不是很系统和全面,可以说是“头皮发麻”,哈哈。因此果断购入《...
    想象美阅读 531评论 1 4
  • 我是小山村一个穷苦人家的孩子,三十年前的一天中午,是我见到世界上的时刻。父母迎着呱呱坠地的我,心里也甚是欢喜,为什...
    longouxiangai阅读 269评论 0 3