前言
上篇文章讲解了多线程的运行状态。本篇文章就来讲讲线程之间的共享。
一、为什么要线程共享
因为线程都是独立的,相互之间是不可见的,所以当两个线程对一个数据进行操作时,就很容易出现问题。
/**
* @version 1.0
* @Description 不同步线程demo
* @Author wb.yang
*/
public class NoSyncDemo {
static Integer count = 0;
public static class CountThread extends Thread {
@Override
public void run() {
count++;
}
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new CountThread().start();
}
System.out.println(count);
}
}

从代码中看出,我们循环了1000次相加count关键字,结果应该是1000,我们看下结果

可见,结果才输出了994,明显跟预期的结果不同。导致这个问题的就是线程的共享,因为线程之间是不不见的,可能线程A拿了count = 100,这时候还没有进行count++的操作,这时候线程B抢到了cpu,也取出了count=100这个时候,就已经数据不同步了。造成了线程安全问题。
针对线程安全问题,我们就要进行线程之间的数据共享。
二、线程共享方式
1.volatile
volatile关键字,最轻量的同步机制。使用volatile关键字,可以使变量成为可见性。保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
/**
* @version 1.0
* @Description volatile线程demo
* @Author wb.yang
*/
public class VolatileSyncDemo {
private volatile static boolean ready;
private static int number;
private static class CountThread extends Thread {
@Override
public void run() {
System.out.println("计数线程启动.......");
//无限循环
while (!ready) ;
System.out.println("number = " + number);
}
}
public static void main(String[] args) {
new CountThread().start();
SleepTools.second(1);
number = 51;
ready = true;
SleepTools.second(5);
System.out.println("main is ended!");
}
}

从代码中可以看出,我们先启动了线程,之间打印了number,然后在主线程中赋值number=51,如果按照线程之间数据不可见的话,那么应该打印null。我们看下结果把。

我们可以看出,输出的赋值后的数据,这事为什么呢?这个就涉及到了volatile关键字,通过volatile关键字,使变量在线程之间可见,所以可以知道number的变化。
2.synchronized
synchronized内置锁,有以下几种锁法
- 普通同步方法,锁是当前对象实例
- 静态同步方法,锁是当前Class对象
- 同步代码块,锁是括号中的参数对象
当一个线程访问同步代码块时,必须先获得锁才能执行同步代码块中的代码,当退出或者抛出异常时,必须要释放锁。
/**
* @version 1.0
* @Description TODO
* @Date 2020/3/25 17:41
*/
public class SynchronizedSyncDemo {
static Integer count = 0;
private static Object object = new Object();
public static class CountThread extends Thread {
@Override
public void run() {
synchronized (object){
count++;
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new CountThread().start();
}
SleepTools.second(2);
System.out.println(count);
}
}

从代码可以看出,我们在run方法中添加了synchronized,并且锁了方法,我们看下结果

我们看到结果是正常的。之所以正常,是因为用了synchronized,来将count++给加锁,进行锁住了,当一个线程加锁锁住这些代码的时候,其他的线程过来必须等待上一个线程执行完代码释放锁,否则不会持有这把锁,只能在这里等待,保证了线程的共享。
3.ThreadLocal
ThreadLocal是在每个线程都使用一个副本,线程之间隔离,操作的是副本中变量的值,只在这个线程中生效,其他线程中的这个变量,又是新的本地副本,所以不会变化
/**
* @version 1.0
* @Description ThreadLocalDemo
* @Author wb.yang
*/
public class ThreadLocalSyncDemo {
static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
public static class CountThread extends Thread {
int id;
public CountThread(int id) {
this.id = id;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":start");
Integer s = threadLocal.get();
s = s + id;
threadLocal.set(s);
System.out.println(Thread.currentThread().getName() + ":"
+ threadLocal.get());
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new CountThread(i).start();
}
}
}

这个是一个ThreadLocal变量,默认值是1,然后启动了三个线程,进行操作。

从结果中看出,每个线程都是单独的处理,县城之间并没有干扰,这个就是ThreadLocal的作用,来保证线程的共享。
总结
线程之间难免要线程共享来保证线程安全,所以就产生了以上几种方法来保证线程同步安全。
网友评论