多线程笔记
1. volatile
1.1 volatile介绍
volatile
保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。它在某些情况下比synchronized
的开销更小。
举个例子我们来分析下面的代码:
public class Main {
public static void main(String[] args) {
VolatileTest volatileTest = new VolatileTest();
new Thread(volatileTest).start();
while (true) {
if (volatileTest.isFlag()){
System.out.println("over");
break;
}
}
}
}
class VolatileTest implements Runnable {
private boolean flag;
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag=" + flag);
}
public boolean isFlag() {
return flag;
}
}
上面的代码最后输出结果是:
flag=true
这个结果是令人诧异的,程序会一直执行while
循环不结束,flag
已经为true
了,为什么while
循环还是不结束呢?说明这里的flag
同时有了两个值
- 在主线程中:
flag=false
- 在副线程中:
flag=true
变量实际是一段内存空间,并不存在同时有两种信息的状态。其实线程在操作主存中的变量数据时,首先会将数据复制到线程私有内存中,当操作完成后才会将数据写回主存,当多个线程操作一个共享变量时,由于线程的修改,导致数据不一致性。就发生了上述结果。
为了解决共享变量的不一致性,使得多线程对共享变量的修改的可见。上述结果我们可以使用synchronized
关键字来解决。如下:
while (true) {
synchronized (volatileTest){
if (volatileTest.isFlag()){
System.out.println("over");
break;
}
}
}
当是这样解决又有一个很大的问题,synchronized
是悲观锁,使得多线程堵塞等待,极大的降低多线程的效率。那有没有一个更好的解决办法呢?这里就可以用到volatile
关键字了,修改如下:
private volatile boolean flag;
只需要将变量flag
声明时,用volatile
修饰就可以保证共享变量flag
的可见性。再次运行就不会发生堵塞数据不一致的问题了。
注意
: 如果将代码改成下面的,运行结果也是没有问题的,导致上面的结果还有一个重要的原因,while
循环中执行的太快,导致主线程来不及去主存中刷新数据。
while (true) {
// 只要是需要消耗一定的时间,让主线程能从主存读取数据即可
System.out.println("no over");
if (volatileTest.isFlag()) {
System.out.println("over");
break;
}
}
1.2 volatile的三大特性:
- 可见性
- 不保证原子性
- 禁止指令重排
具体是如何做到的可以参考以下博客
《死磕Java——volatile的理解》
2. Atomic
jdk1.5
后java.util.concurrent.atomic
包下提供了常用的原子操作类,什么是原子操作呢?顾名思义,就是不可分割的操作。
- i++的原子性问题:i++的操作实际上分为三个步骤
"读-改-写"
int i=10;
i=i++; //10
// 上面的代码等同于下面的
int i = 10;
int temp=i;
i=i+1;
i=temp;
// 所以最后i的值为10
-
原子变量:jdk1.5后java.util.concurrent.atomic包下提供了常用的原子变量:
java.util.concurrent.atomic包
-
volatile
保证内存可见性 -
CAS(Compare-And-Swap)
算法保证数据的原子性CAS
算法是硬件对于并发操作共享数据的支持
CAS
包含了三个操作数:- 内存值
V
- 预估值
A
- 更新值
B
当且仅当V==A
时,V = B
,否则将不做任何操作
- 内存值
可参考博客:
《Java中atomic包中的原子操作类总结》
CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。
3. ConcurrentHashMap
3.1 ConcurrentHashMap 采用"锁分段"机制
Java5.0
在java.util.concurrent
包中提供了多种并发容器类来改进同步容器的性能。ConcurrentHashMap
同步容器类是Java5
增加的一个线程安全的哈希表。对与多线程的操作,介于HashMap
与Hashtable
之间。内部采用“锁分段”机制替代Hashtable
的独占锁。进而提高性能。此包还提供了设计用于多线程上下文中的Collection
实现:
ConcurrentHashMap
、ConcurrentSkipListMap
、ConcurrentSkipListSet
、CopyOnWriteArrayList
和CopyOnWriteArrayset
。当期望许多线程访问一个给定collection
时,ConcurrentHashMap
通常优于同步的HashMap
,ConcurrentSkipListMap
通常优于同步的TreeMap
。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList
优于同步的ArrayList
4. CountDownLatch
4.1 CountDownLatch闭锁
CountDownLatch
一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才继续执行:
- 确保某个计算在其需要的所有资源都被初始化之后才继续执行;
- 确保某个服务在其依赖的所有其他服务都已经启动之后才启动;
- 等待直到某个操作所有参与者都准备就绪再继续执行。
CountDownLatch使用实例代码:
public class Main {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
LatchDemo ld = new LatchDemo(latch);
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
new Thread(ld).start();
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("总时长为:" + (end - start));
}
}
class LatchDemo implements Runnable {
private CountDownLatch latch;
public LatchDemo(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
double random = Math.random() * 100;
if (random > 99) {
System.out.println(random);
}
}
latch.countDown();
}
}
Callable
Callable介绍
Runnable
是执行工作的独立任务,但是它不返回任何值。在Java SE5
中引入的Callable
是一种具有类型参数的泛型,泛型类型是方法call()
的返回的值类型。
四种执行线程方式的介绍
种数 | 种类 | 说明 |
---|---|---|
1 | 实现Runnable 接口 |
通过Thread 实例启动它 |
2 | 继承Thread 类 |
重写Thread 的run 方法 |
3 | 实现Callable 接口 |
通过FutureTask 包装,然后再通过Thread 启动 |
4 | 实现Callable 接口 |
ExecutorServices.submit() |
可参考博客:
《彻底理解Java的Future模式》
《Future模式添加Callback及Promise 模式》
Lock
用于解决多线程安全问题的方式:
-
synchronized
:隐式锁、重量级- 同步代码块
- 同步方法
-
jdk 1.5
后,Lock
:轻量级- 同步锁
Lock
注意:是一个显示锁,需要通过lock()
方法上锁,必须通过unlock()
方法进行释放锁
- 同步锁
多线程安全问题演示
买票案例代码演示:
public class Main {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket,"一号售票窗口").start();
new Thread(ticket,"二号售票窗口").start();
new Thread(ticket,"三号售票窗口").start();
}
}
class Ticket implements Runnable {
private int num = 100;
@Override
public void run() {
while (true) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("卖出一张票,剩余还有:" + --num);
}else if (num == 0){
break;
}
}
}
}
上面的代码存在线程安全问题 ,多线程下对同一共享变量进行修改。
用第一种保证安全性:
while (true) {
synchronized (this){
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("卖出一张票,剩余还有:" + --num);
}else if (num == 0){
break;
}
}
}
但是这样效率严重降低。
用第三种方式保证安全性:
while (true) {
lock.lock();
try{
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("卖出一张票,剩余还有:" + --num);
} else if (num == 0) {
break;
}
}finally {
lock.unlock();
}
}
Condition
编写一个程序,开启
3
个线程,这三个线程的ID
分别为A、B、C
,每个线程将自己的ID
在屏幕上打印10
遍,要求输出的结果必须按顺序显示。
如:ABCABCABC….
依次递归
public class Main {
public static void main(String[] args) {
Test test = new Test();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
test.LoopA();
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
test.LoopB();
}
},"B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
test.LoopC();
}
},"C").start();
}
}
class Test {
private int id = 1;
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void LoopA() {
lock.lock();
try {
while (id != 1) {
condition1.await();
}
System.out.println("A");
id = 2;
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void LoopB() {
lock.lock();
try {
while (id != 2) {
condition2.await();
}
System.out.println("B");
id = 3;
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void LoopC() {
lock.lock();
try {
while (id != 3) {
condition3.await();
}
System.out.println("C");
id = 1;
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
ReadWriteLock 读写锁
写写/读写
需要“互斥”
读读
不需要互斥
public class Main {
public static void main(String[] args) {
Test test = new Test();
for (int i = 0; i < 10; i++) {
int j = i;
new Thread(() -> {
test.write("" + j, new Random().nextInt(10));
}).start();
}
for (int i = 0; i < 100; i++) {
int j = i;
new Thread(() -> {
test.read("" + j);
}).start();
}
}
}
class Test {
private int id = 1;
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public void read(String name) {
rwl.readLock().lock();
try {
System.out.println(String.format("名字:%s,读出的数据:%d", name, id));
} finally {
rwl.readLock().unlock();
}
}
public void write(String name, int id) {
rwl.writeLock().lock();
try {
this.id = id;
System.out.println(String.format("我是写锁,名字:%s,改写数据为:%d", name, id));
} finally {
rwl.writeLock().unlock();
}
}
}
线程八锁
- 两个普通同步方法,两个线程,标准打印,打印?//one two
- 新增 Thread.sleep()给getone(),打印?//one two
- 新增普通方法 getThree(),打印?//three one two
- 两个普通同步方法,两个Number对象,打印?//two one
- 修改 getone()为静态同步方法,打印?//two one
- 修改两个方法均为静态同步方法,一个Number对象?//one two
- 一个静态同步方法,一个非静态同步方法,两个Number对象?//two one
- 两个静态同步方法,两个Number对象?//one two
线程八锁的关键:
- 非静态方法的锁默认为this,静态方法的锁为对应的Class实例
- 某一个时刻内,只能有一个线程持有锁,无论几个方法。
线程池
线程池介绍
线程池:提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提高了响应的速度。
线程池的体系结构:
java.util.concurrent.Executor:负责线程的使用与调度的根接口
|--**ExecutorService子接口:线程池的主要接口
|--ThreadPoolExecutor 线程池的实现类
|--ScheduledExecutorService 子接口:负责线程的调度
|--ScheduledThreadPoolExecutor:继承 ThreadPoolExecutor,
实现 ScheduledExecutorService
工具类:Executors
ExecutorService newFixedThreadPool():创建固定大小的线程池
ExecutorService newCachedThreadPool():缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。
ExecutorService newSingleThreadExecutor():创建单个线程池。线程池中只有一个线程
ScheduledExecutorService newScheduledThreadPool():创建固定大小的线程,可以狂迟或定时的执行任务。