什么是JUC
在 Java 5.0 (JDK 1.5)
提供了 java.util.concurrent
( 简称JUC
) 包,在此包中增加了在并发编程中很常用的实用工具类, 用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。
Java 创建多线程的三种方式
在Java项目中参加多线程有四种方式,如果包括从线程池中创建线程的话就包含四种方式了,这里只介绍三种创建线程的方式:
继承Thread类
、实现Runnable接口
、实现Callable接口
继承Thread
这里主要是继承
Thread
类,然后重写run
方法。
public class UseJavaThread {
public static void main(String[] args) {
CustomThread customThread = new CustomThread();
CustomThread customThread2 = new CustomThread();
CustomThread customThread3 = new CustomThread();
customThread.start();
customThread2.start();
customThread3.start();
}
}
class CustomThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+" : "+i);
}
}
}
实现Runnable接口
覆写Runnable接口实现多线程可以避免单继承局限,当子类实现Runnable接口,此时子类和Thread的代理模式(子类负责真是业务的操作,thread负责资源调度与线程创建辅助真实业务。
public class UseJavaThread {
public static void main(String[] args) {
CustomThread2 customThread2 = new CustomThread2();
new Thread(customThread2,"线程1").start();
new Thread(customThread2,"线程2").start();
new Thread(customThread2,"线程3").start();
}
}
class CustomThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+" : "+i);
}
}
}
实现Callable接口
覆写Callable接口实现多线程(JDK1.5),核心方法是call()方法,有返回值。需要有一个FutureTask对象包装,可以接收它的返回值。
public class UseJavaThread {
public static void main(String[] args) {
CustomThread3 customThread3 = new CustomThread3();
FutureTask<String> futureTask = new FutureTask<>(customThread3);
new Thread(futureTask,"线程1").start();
try {
System.out.println(futureTask.get());
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class CustomThread3 implements Callable<String> {
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 0; i < 3; i++) {
sum = sum+i;
}
return "sum :"+sum;
}
}
一个简单的Demo
public class DemoThreadTest {
public static void main(String[] args) {
System.out.println("线程启动====>");
ThreadDemo threadDemo = new ThreadDemo();
new Thread(threadDemo).start();
while (true){
if (threadDemo.isFlag()){
System.out.println("thread demo's flag is true");
break;
}
}
}
}
class ThreadDemo implements Runnable{
/**
* 一个标识符
*/
private boolean flag = false;
@Override
public void run() {
/**
* 1.5秒后,将flag变量设置成true
*/
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("set flag = true");
setFlag(true);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
上面这段是一段简单的代码,类ThreadDemo
实现了Runnable
接口,其中有个标识变量flag
,线程启动1.5秒之后,会将flag
变量的值设置成true
。在main方法中,创建了一个线程去执行ThreadDemo
的实例,并创建了一个循环去监测ThreadDemo
实例中的flag
变量,当flag
变量为true
的时候,会打印输出一段话,并跳出该循环。
我们从阅读代码,我们预测执行结果应该会是:
线程启动====>
# 1.5 秒之后
set flag = true
thread demo's flag is true
# 程序结束
实际执行结果:
线程启动====>
# 1.5秒之后
set flag = true
# 程序阻塞中...
为什么会出现这样的结果呢?明明我们创建了一个实例,一个新线程中1.5秒后修改该实例的flag
,但是为什么主线程中没有监测到flag
变量的改变呢?转眼给我们要回到内存可见性
相关了。
内存可见性
简介
当多个线程操作共享数据时,彼此不可见。
内存可见性(Memory Visibility)是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。
执行原理
多线程的使用中,当一个类实例化成功后,它的变量会初始化到主存
(堆内存)中,但是当其他线程创建时,jvm会为每个线程分配独立的缓存
,为其提高效率,我们称之为线程缓存
。
执行分析
上面我们的程序执行过程中,实例化了ThreadDemo
的实例,flag
变量会保存在主存
中,此时的flag
变量的值为false
。当线程创建成功后(我们称为线程一),会将主内存中的flag
变量读取到线程缓存
中来,此时主存
中的flag
的值为false
,然后主线程中的循环要获取flag变量时,也是从主内存中将flag复制到了主线程的内存中,此时主内存中的flag
依然是false
,1.5秒后,线程一中将线程一内存的flag变量修改为true,并更新到主存
中,但是主线程中是使用while循环
一直获取的是自己线程缓存
中的flag,所以并不知道flag已经为true了。
要让while循环中每次都获取的都是主内存中的变量,我们可以加锁:
public class DemoThreadTest {
public static void main(String[] args) {
System.out.println("线程启动====>");
ThreadDemo threadDemo = new ThreadDemo();
new Thread(threadDemo).start();
while (true){
synchronized (threadDemo){
if (threadDemo.isFlag()){
System.out.println("thread demo's flag is true");
break;
}
}
}
}
}
...
加了锁,就可以让while循环每次都从主存中去读取数据,这样就能读取到true了。但是一加锁,每次只能有一个线程访问,当一个线程持有锁时,其他的就会阻塞,效率就非常低了。
volatile 关键字
简介
Java 提供了一种稍弱的同步机制,即 volatile
变量,用来确保将变量的更新操作通知到其他线程。当多个线程操作共享数据时,可以保证内存中的数据可见。用这个关键字修饰共享数据,就会及时的把线程缓存
中的数据刷新到主存
中去,并通知其他线程。可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:
- 对于多线程,不是一种互斥关系
- 不能保证变量状态的“原子性操作”
修改实例
package com.martain.study;
/**
* @author Martin
* @version 1.0
* @date 2020/7/6 5:25 下午
*/
public class VolatileKeyWorldTest {
/**
* validate 关键字:当多个线程进行操作共享数据时,可以保存内存中的数据可见
* 相较于synchronized 是一种较为轻量级的同步策略,
* 注意:
* 1、volatile不具备"互斥性"
* 2、不能保证变量的原子性
*
* @param args
*/
public static void main(String[] args) {
ThreadVolatileDemo threadDemo = new ThreadVolatileDemo();
new Thread(threadDemo).start();
while (true){
if (threadDemo.isFlag()){
System.out.println("thread demo's flag is true");
break;
}
}
}
}
class ThreadVolatileDemo implements Runnable{
private volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("set flag = true");
setFlag(true);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
我们只需要在变量前添加
volatile
关键字就可以将变量的更新操作通知到其他线程。就可以保证多个线程中所使用的变量都是最新的。
原子性与CAS算法
原子性
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。即在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
i++的原子性问题
i++的操作实际分为三个步骤”读-改-写“
// i++ 的底层操作
int temp = i
i = i + 1
i = temp
变量多线程安全性问题
public class AtomicTest {
public static void main(String[] args) {
ThreadAtomic threadAtomic = new ThreadAtomic();
for (int i = 0; i < 10; i++) {
new Thread(threadAtomic,"thread "+i).start();
}
}
}
class ThreadAtomic implements Runnable{
int serial = 0;
@Override
public void run() {
try {
Thread.sleep(200);
System.out.println(getSerial());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 每次访问加一
* @return
*/
public int getSerial(){
return serial++;
}
}
上面这个例子中,每次创建线程执行一次
getSerial
方法就会获取到serial
的值,并让serial
自增1,创建了10个线程来启动ThreadAtomic
实例,正常来讲,应该会输出0~9 这10个数字,因为线程执行顺序的原因可能顺序不是固定的,但是数字应该不会重复。但是实际的结果是有一定情况有一些数字重复的,因为不具备互斥性
,导致多个线程可能有某几个线程同时
获取了这个变量,导致他们获取的变量都是一样的,这就是多线程的安全问题。要解决这个问题可以对变量加锁,让同一时刻只能有一个线程对其进行操作,都是锁机制太耗费性能了,会对其他线程进行阻塞,我们可以使用性能更好的CAS算法来解决这个问题,CAS算法性能好是因为她不会阻塞线程
。
CAS算法
CAS (Compare-And-Swap
) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。
CAS 是一种无锁的非阻塞算法的实现。CAS 包含了 3 个操作数:
- 读写的内存值 V
内存值
- 进行比较的值 A
预估值
- 拟写入的新值 B
更新值
当且仅当 V 的值等于 A 时, CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作
原子变量
上面我们讲到了什么是原子性,java.util.concurrent.atomic
包下提供了如下一些原子操作的常用类:
- AtomicBoolean 、 AtomicInteger 、 AtomicLong 、 AtomicReference
- AtomicIntegerArray 、 AtomicLongArray
- AtomicMarkableReference
- AtomicReferenceArray
- AtomicStampedReference
类 AtomicBoolean、 AtomicInteger、 AtomicLong 和 AtomicReference 的实例各自提供对
相应类型单个变量的访问和更新。每个类也为该类型提供适当的实用工具方法。
AtomicIntegerArray、 AtomicLongArray 和 AtomicReferenceArray 类进一步扩展了原子操
作,对这些类型的数组提供了支持。这些类在为其数组元素提供 volatile 访问语义方
面也引人注目,这对于普通数组来说是不受支持的。
这些原子变量的原理是使用了
volatile
保证了其内存的可见性,使用CAS算法保证了数据的原子性,它们的核心方法:boolean compareAndSet(expectedValue, updateValue)
使用原子变量
public class AtomicTest {
public static void main(String[] args) {
ThreadAtomic threadAtomic = new ThreadAtomic();
for (int i = 0; i < 10; i++) {
new Thread(threadAtomic,"thread "+i).start();
}
}
}
class ThreadAtomic implements Runnable{
AtomicInteger serial = new AtomicInteger(0);
@Override
public void run() {
try {
Thread.sleep(200);
System.out.println(getSerial());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 每次访问加一
* @return
*/
public int getSerial(){
return serial.getAndIncrement();
}
}
1
0
2
3
5
9
4
7
8
6
使用原子变量修改了上面的实例之后,发现结果正常了,解决了基本变量的在多线程中的共享数据的安全问题。
网友评论