1.多行程带来的风险
上一篇我们提到了线程的优点,和生活中一样,万物都有相对的一面,它给我们带来便利的同时,也有不安全性
上代码
public static void main1(String[] args)
private static class Counter {
private long n = 0;
public void increment() {
n++;
}
public void decrement() {
n--;
}
public long value() {
return n;
}
}
public static void main(String[] args) throws InterruptedException {
final int COUNT = 1000_0000;
Counter counter = new Counter();
Thread thread = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
counter.increment();
}
});
thread.start();
for (int i = 0; i < COUNT; i++) {
counter.decrement();
}
thread.join();
System.out.println(counter.value());
}
代码的预期结果应该为0,但是执行代码后,我们会发现,每次的执行结果都不一样,但都不是0,为什么会产生这样的结果呢?
都是“抢占式” 的锅,在某个线程抢夺到cpu的时间片后,执行代码的时候可能给n已经+1或者-1了但是没有写到内存中,时间片用完了,两个线程又开始抢夺cpu了,这样的过程使得运行结果不等于0
可以看看下面的一张图加深理解
1.线程不安全的原因
1.原子性
什么是原子性
我们把一段代码想象成一个厕所,每个线程就是要进入这个厕所的人。如果没有任何机制保证,A进入厕所之后,还没有出来;B 是不是也可以进入厕所,打断 A 在上厕所?。这个就是不具备原子性的。那我们应该如何解决这个问题呢?是不是只要给厕所加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
一条 java 语句不一定是原子的,也不一定只是一条指令
比如刚才我们看到的 n++,其实是由三步操作组成的:
- 从内存把数据读到 CPU
- 进行数据更新
- 把数据写回到 CPU
2 .可见性
为了提高效率代码的执行效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,他发现当前线程没有改变这个值,就不会一直读取该数据,而是将刚才读取到的直接使用,这个就是可见性问题。
3.代码重排序
jvm会将我们的代码进行优化,可能导致在运行过程中,代码的执行不是从上到下的
例如一段代码是这样的:
- 去前台取下 U 盘
- 去教室写 10 分钟作业
- 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序
2.线程安全的解决
1.synchronized 关键字-监视器锁monitor lock
synchronized的底层是使用操作系统的mutex lock实现的。
- 当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
synchronized用的锁是存在Java对象头里的。
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
修改上面的代码
public class ThreadDemo {
private static class Counter {
private long n = 0;
public synchronized void increment() {
n++;
}
public synchronized void decrement() {
n--;
}
public synchronized long value() {
return n;
}
}
public static void main(String[] args) throws InterruptedException {
final int COUNT = 1000_0000;
Counter counter = new Counter();
Thread thread = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
counter.increment();
}
});
thread.start();
for (int i = 0; i < COUNT; i++) {
counter.decrement();
}
thread.join();
// 期望最终结果应该是 0
System.out.println(counter.value());
}
}
这样操作之后thread线程和mian线程会等其中一方释放锁之后再进行抢夺cpu时间片
2.volatile 关键字
它解决了我们刚才说的可见性问题,加上volatile后jvm就不会每次都会重cpu中读取该变量
3.线程间通信
线程间是怎么进行通信的呢?
java中提供了几个方法实现了线程间的通信
- wait()方法
其实wait()方法就是使线程停止运行
- 方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
- wait()方法只能在同步方法中或同步块中调用。如果调用wait()时,没有持有适当的锁,会抛出异常。
- wait()方法执行后,当前线程释放锁,线程与其它线程竞争重新获取锁。
观察wait的使用
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中...");
object.wait();
System.out.println("等待已过...");
}
System.out.println("main方法结束...");
}
我们会发现程序永远不会执行“等待已过…”,“main方法结束…”,这是因为我们让线程进入等待,没通知它啥时候就可以不用等待了,这时候就要看看notify()
- notify()
notify方法就是使停止的线程继续运行。
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对
其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个
呈wait状态的线程。- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出
同步代码块之后才会释放对象锁。
还有一个notifyAll()方法,该方法与notify都是唤醒等待的线程,不过notify是唤醒一个等待的线程,notifAll是唤醒所有等待的线程
4.wait和sleep的区别
其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,
唯一的相同点就是都可以让线程放弃执行一段时间。
- wait 之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是 wait 对象上的 monitor lock
- sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求。
- wait 是 Object 的方法
- sleep 是 Thread 的静态方法
多线程的练习:
1.定时器的实现
public class Task implements Comparable<Task> {
private Runnable command;
private long time;//绝对时间
//after 指的是多少ms后执行 即相对时间
public Task(Runnable command, long after) {
this.command = command;
this.time = after+System.currentTimeMillis();
}
public void run(){
command.run();
}
public long getTime() {
return time;
}
@Override
public int compareTo(Task o) {
return (int)(this.time-o.time);
}
}
import java.util.concurrent.PriorityBlockingQueue;
public class Worker extends Thread {
private PriorityBlockingQueue <Task> queue = null;
private Object mailBox = null;
public Worker(PriorityBlockingQueue<Task> queue,Object mailBox){
this.queue = queue;
this.mailBox = mailBox;
}
@Override
public void run() {
while(true){
try {
Task task =queue.take();
long curTime = System.currentTimeMillis();
if(curTime<task.getTime()){
queue.put(task);
synchronized (mailBox){
mailBox.wait(task.getTime()-curTime);
}
}else{
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
import java.util.concurrent.PriorityBlockingQueue;
public class Timer {
//由四个部分组成
//1.有一个类描述“任务”
//2.有一个阻塞优先队列来管理这些任务
//3.有一个线程来扫描这个队列,判断是否队首“任务”应该执行了
//4.有一个接口来让调用者"安排"任务
PriorityBlockingQueue<Task> queue =new PriorityBlockingQueue<>();
Object mailBox = new Object();
public Timer() {
Worker worker = new Worker(queue,mailBox);
worker.start();
}
public void schedule(Runnable runnable,long time){
queue.put(new Task(runnable,time));
synchronized (mailBox){
mailBox.notify();
}
}
public static void main(String[] args) {
Timer timer =new Timer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hehe");
timer.schedule(this,2000);
}
},2000);
}
}
2.线程池的实现
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class MyThreadPool {
//用阻塞队列来管理任务
BlockingQueue<Runnable> queue =new LinkedBlockingQueue<>();
//使用List来管理工作线程
List<Worker> workers =new ArrayList<>();
public static final int MAXCOUNT =10;
public void execute(Runnable command) throws InterruptedException {
if(workers.size()<MAXCOUNT){
Worker worker =new Worker(queue,workers.size());
workers.add(worker);
worker.start();
}
queue.put(command);
}
public void shutDown() throws InterruptedException {
for(Worker worker :workers){
worker.interrupt();
}
for(Worker worker :workers){
worker.join();
}
}
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool =new MyThreadPool();
for(int i = 0; i < 1000; i++){
Command command =new Command(i);
myThreadPool.execute(command);
}
Thread.sleep(2000);
myThreadPool.shutDown();
System.out.println("线程池已被销毁");
}
}
import java.util.concurrent.BlockingQueue;
public class Worker extends Thread{
BlockingQueue<Runnable> queue =null;
int id ;
public Worker(BlockingQueue<Runnable> queue,int id) {
this.queue = queue;
this.id = id;
}
@Override
public void run() {
try {
while(!Thread.currentThread().isInterrupted()){
Runnable command = queue.take();
System.out.println("Thread " + id + " Running...");
command.run();
}
} catch (InterruptedException e) {
System.out.println("线程结束");
}
}
}
public class Command implements Runnable{
public int id;
public Command(int id) {
this.id = id;
}
@Override
public void run() {
System.out.println("正在执行任务: " + id);
}
}
总结-保证线程安全的思路
- 使用没有共享资源的模型
- 适用共享资源只读,不写的模型
- 不需要写共享资源的模型
- 使用不可变对象
- 直面线程安全
- 保证原子性
- 保证顺序性
- 保证可见性