1. 并发编程
1.1并发编程学习目的
工作需要
1.2线程安全
- 概念
当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全. - synchronized
可以在任意对象及方法上加锁,而加锁的这段代码称为”互斥区”或”临界区”
1.7 volatile关键字
- 概念
使变量在多个线程间可见 - 总结
- 在java中,每一个线程都有一块工作内存区,其中存放着所有线程共享的主内存中的变量值的拷贝.当线程执行时,他在自己的工作内存区中操作这些变量.为了存取一个共享变量,一个线程通常先获取锁定并去清除他的内存工作区,把这些共享变量从所有线程的共享内存区中正确的装入到他自己所在的工作内存区中,当线程解锁时保证该工作内存区中的变量的值写回到共享内存中.(
RunThread
) - 一个线程可以执行的操作有使用(use),赋值(assign),装载(load),存储(store),锁定(lock),解锁(unlock).
- 而主线程可以执行的操作有读(read),写(write),锁定(lock),解锁(unlock),每个操作都是原子的.
- volatile的作用就是强制线程到主内存(共享内存)里去读取变量,而不是到线程工作区里去读取,从而实现多个线程间的变量可见,也就满足了线程安全的可见性.
- volatile具有多线程间的可见性,但不具有原子性,想实现原子性,用AtomicInteger.(
VolatileNoAtomic
) - 但是atomic类只保证本身方法的原子性,并不保证多次操作的原子性.(
AtomicUse
)
- 在java中,每一个线程都有一块工作内存区,其中存放着所有线程共享的主内存中的变量值的拷贝.当线程执行时,他在自己的工作内存区中操作这些变量.为了存取一个共享变量,一个线程通常先获取锁定并去清除他的内存工作区,把这些共享变量从所有线程的共享内存区中正确的装入到他自己所在的工作内存区中,当线程解锁时保证该工作内存区中的变量的值写回到共享内存中.(
public class RunThread extends Thread {
private volatile boolean isRunning=true;
private void setRunning(boolean isRunning){
this.isRunning = isRunning;
}
public void run(){
System.out.println("进入线程");
while(isRunning==true){
}
System.out.println("线程停止");
}
public static void main(String[] args) throws InterruptedException {
RunThread rt = new RunThread();
rt.start();
Thread.sleep(2000);
rt.setRunning(false);
System.out.println("isRunning被设置成了false");
Thread.sleep(1000L);
System.out.println(rt.isRunning);
}
}
public class VolatileNoAtomic extends Thread {
// private static volatile int count;
private static AtomicInteger count = new AtomicInteger(0);
private static void addCount(){
for (int i = 0; i < 1000; i++) {
// count++;
count.incrementAndGet();
}
System.out.println(count);
}
public void run(){
addCount();
}
public static void main(String[] args) {
VolatileNoAtomic[] arr = new VolatileNoAtomic[10];
for (int i = 0; i < 10; i++) {
arr[i] = new VolatileNoAtomic();
}
for (int i = 0; i < 10; i++) {
arr[i].start();
}
}
}
public class AtomicUse {
private static AtomicInteger count = new AtomicInteger(0);
/**
* 多个addAndGet在同一个方法内是非原子性的,需要加synchronized修饰,
* 保证四个addAndGet整体原子性
* @return
*/
public synchronized int multiAdd(){
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
count.addAndGet(1);
count.addAndGet(2);
count.addAndGet(3);
count.addAndGet(4);
return count.get();
}
public static void main(String[] args) {
AtomicUse atomicUse = new AtomicUse();
ArrayList<Thread> ts = new ArrayList<>();
for (int i = 0; i <100 ; i++) {
ts.add(new Thread(new Runnable(){
@Override
public void run() {
System.out.println(atomicUse.multiAdd());
}
}));
}
for(Thread t:ts){
t.start();
}
}
}
2. 线程通信
2.1线程之间通信
- 线程通信概念
线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体,线程间的通信就成为整体的必用方式之一.当线程存在通信指挥,系统间的交互性会更强大,在提高CPU利用率的同时还会使开发人员对线程任务在处理的过程中进行有效的把控与监督.(ListAdd1
) - 使用wait和notify方法实现线程间的通信,(注意这两个方法都是object的类方法,换句话说java为所有的对象都提供了这两个方法)(
ListAdd2
)
wait和notify必须配合synchronized关键字使用
wait方法释放锁,notify方法不释放锁 - notify不释放锁,导致不实时,所以用其他方法解决,使CountDownLatch(
ListAdd3
),CountDownLatch里面的数字表示通知几次,对应countDownLatch.countDown()的执行次数.
import java.util.ArrayList;
import java.util.List;
public class ListAdd1 {
volatile static List list = new ArrayList<String>();
public void addList(){
list.add("元宝");
}
private int getSize(){
return list.size();
}
public static void main(String[] args) {
ListAdd1 list1 = new ListAdd1();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
list1.addList();
System.out.println("当前线程"+Thread.currentThread().getName()+"添加了一个元素");
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
if (list1.getSize()==5){
System.out.println("当前线程收到通知:"+Thread.currentThread().getName()+"listSize=5 线程停止");
throw new RuntimeException();
}
}
}
},"t2");
t1.start();
t2.start();
}
}
public class ListAdd2 {
volatile static List list = new ArrayList<String>();
public void addList(){
list.add("元宝");
}
private int getSize(){
return list.size();
}
public static void main(String[] args) {
ListAdd2 list2 = new ListAdd2();
Object lock = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (lock){
for (int i = 0; i < 10; i++) {
list2.addList();
System.out.println("当前线程"+Thread.currentThread().getName()+"添加了一个元素");
Thread.sleep(500);
if (list2.getSize()==5){
System.out.println("已经发出通知...");
lock.notify();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock){
if (list2.getSize()!=5){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("当前线程收到通知:"+Thread.currentThread().getName()+"listSize=5 线程停止");
throw new RuntimeException();
}
}
},"t2");
t2.start();
t1.start();
}
}
public class ListAdd3 {
volatile static List list = new ArrayList<String>();
public void addList(){
list.add("元宝");
}
private int getSize(){
return list.size();
}
public static void main(String[] args) {
final CountDownLatch countDownLatch = new CountDownLatch(1);
ListAdd3 list3 = new ListAdd3();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
list3.addList();
System.out.println("当前线程"+Thread.currentThread().getName()+"添加了一个元素");
Thread.sleep(500);
if (list3.getSize()==5){
System.out.println("已经发出通知...");
countDownLatch.countDown();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
if (list3.getSize()!=5){
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("当前线程收到通知:"+Thread.currentThread().getName()+"listSize=5 线程停止");
throw new RuntimeException();
}
},"t2");
t2.start();
t1.start();
}
}
2.2 使用wait和notify模拟queue
- BlockingQueue:顾名思义,首先它是一个队列,并且支持阻塞的机制,阻塞的放入和得到数据,我们要实现LinkedBlockingQueue下面两个简单的方法put和take.
put(anObject): 把anObject加入到BlockingQueue里面,如果BlockingQueue没有空间,则调用此方法的线程被阻断,知道BlockingQueue里面有空间在继续.
take: 取走BlockingQueue里面排在首位的对象,若BlockingQueue为空,阻断进入等待状态,知道BlockingQueue有新的数据被加入.
public class MyQueue {
private LinkedList<Object> list = new LinkedList<>();
private AtomicInteger count = new AtomicInteger(0);
private final int minSize = 0;
private final int maxSize ;
public MyQueue(int maxSize){
this.maxSize = maxSize;
}
private final Object lock = new Object();
public void put(Object obj){
synchronized (lock){
while(count.get()==this.maxSize){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(obj);
count.incrementAndGet();
System.out.println("新加入的元素为:"+obj);
lock.notify();
}
}
public Object take(){
Object ret = null;
synchronized (lock){
while(count.get()==minSize){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
ret = list.remove();
count.decrementAndGet();
lock.notify();
}
return ret;
}
public int getSize(){
return this.count.get();
}
public static void main(String[] args) {
MyQueue mq = new MyQueue(5);
mq.put("天王");
mq.put("地虎");
mq.put("小鸡");
mq.put("蘑菇");
mq.put("糗百");
System.out.println("当前容器的长度"+mq.getSize());
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
mq.put("a");
mq.put("b");
}
},"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");
t2.start();
}
}
2.3ThreadLocal
ThreadLocal概念:
线程局部变量,是一种多线程间并发访问变量的解决方案.预期synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而是使用空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全.
- 从性能上说,ThreadLocal不具有绝对优势,在并发不是很高的时候,加锁的性能会更好,但作为一套与锁完全无关的线程安全解决方案,在高并发量或者竞争激烈的场景,使用ThreadLocal可以在一定程度上减少锁竞争.
public class ConnThreadLocal {
public static ThreadLocal<String> th=new ThreadLocal<>();
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) {
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 (InterruptedException e) {
e.printStackTrace();
}
}
},"t2");
t1.start();
t2.start();
}
}
2.3单例&多线程
单例模式最常见的就是饥饿模式和懒汉模式,一个直接实例化对象,一个在调用方法时实例化对象.在多线程模式中,考虑到性能和线程安全问题,我们一般选择下面两种比较经典的单例模式,在性能提高的同时,又保证了线程安全.
- dubble check instance
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();
}
}
- static inner class
public class InnerSingleton {
private static class Singlton{
private static Singlton singlton = new Singlton();
}
public static Singlton getInstance(){
return Singlton.singlton;
}
}
3.1 同步类容器
- 同步类容器都是线程安全的,但在某些情况下可能需要加锁来保护复合操作,复合类操作如:迭代(反复访问元素,遍历容器中所有的元素),跳转(根据指定的顺序找到当前元素的下一个元素),以及条件运算.这些复合操作在多线程并发的修改容器时,可能会出现意外的行为,最经典的便是ConcurrentModificationException,原因是当容器的迭代的过程中,被并发的修改了内容,这是由于早期迭代器设计的时候并没有考虑到并发修改的问题.
- 同步类容器:如古老的Vector,HashTable.这些容器的同步功能其实都是由JDK的Collections.synchronized***等工厂方法去实现的.其底层的机制无非就是用传统的synchronized关键字对每个公用的方法都进行同步,使得每次只能有一个线程访问容器的状态.这很明显不满足我们今天互联网时代高并发的需求,在保证线程安全的同时,也必须要有足够好的性能.
public class Tickets {
public static void main(String[] args) {
Vector<String> tickets = new Vector<>();
for (int i = 0; i < 10; i++) {
tickets.add("火车票"+i);
}
for(Iterator iterator = tickets.iterator();iterator.hasNext();){
String string = (String) iterator.next();
tickets.remove(2);
}
}
}
3.2 并发类容器
- JDK5.0以后提供了多种并发类容器来代替同步类容器来改善性能.同步类容器的状态都是串行化的,他们虽然实现了线程安全,但是严重降低了并发性,在多线程环境时,严重降低了应用程序的吞吐量.
- 并发类容器时专门针对并发设计的,使用ConcurrentHashMap来代替给予散列的传统的HashTable,而且在ConcurrentHashMap中,添加了一些常见复合操作的支持.以及使用了CopyOnWriteArrayList来代替Voctor,并发的CopyOnWriteArraySet,以及并发的Queue,ConcurrentLinkedQueue和LinkedBlockingQueue,前者是高性能的对列,后者是以阻塞形式的队列,具体实现Quene还有很多,例如ArrayBlockingQueue,PriorityBlockingQueue和SynchronousQueue等.
4.1ConcurrentMap
- ConcurrentMap接口下有两个重要的实现
- ConcurrentHashMap
- ConcurrentSkipListMap(支持并发排序功能,弥补ConcurrentHashMap)
- ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的HashTable,他们有自己的锁.只要多个修改操作发生自不同的段,他们就可以并发进行.把一个整体分成16个段,也就是支持16个线程并发修改操作.这也是在多线程场景时减小锁的粒度从而降低锁竞争的一种方案.并且代码中大多共享变量使用volatile关键字声明,目的是第一时间获取修改的内容,性能非常好.
4.2Copy-On-Write容器
- Copy-On_Write简称COW是一种用于程序设计中的优化策略.
- JDK里的COW容器有两种:CopyOnWriteArrayList和CopyOnWriteArraySet,COW容器非常有用,可以在非常多的并发场景中使用.
- 什么是CopyOnWrite容器?
CopyOnWrite容器即写时复制容器.通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前的容器Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器.这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素.所以CopyOnWrite也是一种读写分离的思想,读和写不同的容器.
6.1多线程设计模式
并行设计模式属于设计优化的一部分,他是对一些常见多线程结构的总结和抽象.与串行结构相比,并行程序的结构通常更为复杂.因此,合理的使用并行结构,在这里主要介绍Future,Master-Worker