一.volatile关键字
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。如下实例:
public class JUCTest1{
public static void main(String[] args) {
TreeDemo treeDemo = new TreeDemo();
new Thread(treeDemo).start();
while (true){
if (treeDemo.isFlag()){
System.out.println("-------------------");
System.out.println("Flag = " + treeDemo.isFlag());
}
}
}
}
class TreeDemo implements Runnable{
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("Flag = " + isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
原本期望是在treeDemo睡眠期间,main线程由于while一直查看flag状态为false,所以没有输出,当treeDemo线程开始执行,并将flag值修改为true后,main线程应当开始输出,但结果与预期不符。
如图和代码,让TreeDemo线程睡眠一下,此时main线程的缓存已经将flag == false保存,这时treeDemo线程再将flag保存到线程并修改flag的值,可能出现treeDemo所持有的缓存中flag的值更改了,主存中的flag值更改了,但是main线程持有的缓存中的值未能被修改。因为main线程的缓存是对其他线程不可见的。
当一个变量定义为volatile之后,将具备两种特性:
1⃣️保证此变量对所有的线程的可见性,这里的“可见性”理解为,当一个线程修改了这个变量的值,volatile保证了新值能立即同步到主存内,以及每次使用前立即从主存内刷新。但普通变量做不到这点,普通变量的值在线程传递均需要通过主内存来完成。
2⃣️禁止指令重新序列化。有volatile修饰的变量,赋值后多执行了一个“内存屏障”操作。
volatile性能:
volatile的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在贝蒂代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
二.原子变量
问题引出:
public class JUCTest2 {
public static void main(String[] args) {
AtomicDemo atomicDemo = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(atomicDemo).start();
}
}
}
class AtomicDemo implements Runnable{
private int serialNumber = 0;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + getSerialNumber());
}
public int getSerialNumber() {
return serialNumber++;
}
public void setSerialNumber(int serialNumber) {
this.serialNumber = serialNumber;
}
}
i++实际上是三步操作,分为:temp = i; i = i+1; i = temp; 出现了类volatile关键字问题,当线程A的sn还没能来得及改写主存中的sn时,线程B读取了主存中的sn并加一输出,所以出现了相同的数。但这次的问题加上volatile之后并没能解决。因为完成i++操作是三步不可分割的读改写操作,但是volatile只是保证了可见性问题,不能保证两个线程在运行各自的三步操作时,先运行完一个完整的读改写再运行另一个。由此引出原子变量。
原子变量: jdk1.5之后java.util.concurrent.atomic包下提供了常用的原子变量。
原子变量的特性:
1⃣️原子变量具备volatile特性,其包装的值都用volatile修饰。保证了内存可见性。
2⃣️通过CAS(Compare And Swap)算法保证数据的原子性。【CAS算法包含了三个操作数:内存值,预估值,更新值,当且仅当内存值 == 预估值时,才把更新值= 内存值】
使用原子变量后的代码:[ 仅修改private int serialNumber = 0; 类似于包装类的使用方法。 ]
public class JUCTest2 {
public static void main(String[] args) {
AtomicDemo atomicDemo = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(atomicDemo).start();
}
}
}
class AtomicDemo implements Runnable{
private AtomicInteger serialNumber = new AtomicInteger();
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + getSerialNumber());
}
public int getSerialNumber() {
return serialNumber.getAndIncrement();
}
}
得到正确结果。
三.CountDownLatch 闭锁
一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才继续执行:
1⃣️确保某个计算在其需要的所有的资源都被初始化之后才继续执行。
2⃣️确保某个服务在其他依赖的所有其他服务都已经启动之后才启动。
3⃣️等待直到某个操作所有参与者都准备就绪在继续执行。
实例代码:
开始5个线程分别查处5万以内的所有偶数,想要从main线程中计算,执行完5个线程所要消耗的时间。
public class LatchDemo {
public static void main(String[] args) {
final CountDownLatch la = new CountDownLatch(5);
Latch latch = new Latch(la);
long start = System.currentTimeMillis();
for (int i = 0; i < 5; i++) {
new Thread(latch).start();
}
try {
la.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("耗费时间为 : " + (end - start));
}
}
class Latch implements Runnable{
private CountDownLatch latch;
public Latch(CountDownLatch latch){
this.latch = latch;
}
@Override
public void run() {
synchronized (this){
try {
for (int i = 0; i < 50000; i++) {
if (i % 2 == 0){
System.out.println(i);
}
}
}finally {
latch.countDown();
}
}
}
}
四.Callable接口
实现Callable重写call方法和实现Runnable类似,其特点表现在:
a.可以在任务结束后提供一个返回值。
b.call方法可以抛出异常。
c.可以通过Callable得到的Fulture对象监听目标线程调用call方法的结果,得到返回值。
public class CallableTest {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
//执行Callable方式,需要FutureTask实现类的支持,用于接收运算结果
//FutureTask是Future接口的实现类
FutureTask<Integer> result = new FutureTask<>(threadDemo);
new Thread(result).start();
//接收线程运算后的结果
try {
Integer sum = result.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class ThreadDemo implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
System.out.println(i);
sum += i;
}
return sum;
}
}
使用Callable接口实现多线程的步骤:
1⃣️创建Callable子类的实例化对象。
2⃣️创建FutureTask对象,并将Callable对象传入FutureTask的构造方法中。
3⃣️实例化Thread对象,并在构造方法中传入FutureTask对象。
4⃣️启动线程。