快速入门多线程(初阶二)

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++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU
  2. 进行数据更新
  3. 把数据写回到 CPU

2 .可见性
为了提高效率代码的执行效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,他发现当前线程没有改变这个值,就不会一直读取该数据,而是将刚才读取到的直接使用,这个就是可见性问题。
3.代码重排序
jvm会将我们的代码进行优化,可能导致在运行过程中,代码的执行不是从上到下的

例如一段代码是这样的:

  1. 去前台取下 U 盘
  2. 去教室写 10 分钟作业
  3. 去前台取下快递
    如果是在单线程情况下,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中提供了几个方法实现了线程间的通信

  1. wait()方法

其实wait()方法就是使线程停止运行

  1. 方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
  2. wait()方法只能在同步方法中或同步块中调用。如果调用wait()时,没有持有适当的锁,会抛出异常。
  3. 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()

  1. notify()

notify方法就是使停止的线程继续运行。

  1. 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对
    其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个
    呈wait状态的线程。
  2. 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出
    同步代码块之后才会释放对象锁。

还有一个notifyAll()方法,该方法与notify都是唤醒等待的线程,不过notify是唤醒一个等待的线程,notifAll是唤醒所有等待的线程

4.wait和sleep的区别
其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,
唯一的相同点就是都可以让线程放弃执行一段时间。

  1. wait 之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是 wait 对象上的 monitor lock
  2. sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求。
  3. wait 是 Object 的方法
  4. 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);
    }
}

总结-保证线程安全的思路

  1. 使用没有共享资源的模型
  2. 适用共享资源只读,不写的模型
    1. 不需要写共享资源的模型
    2. 使用不可变对象
  3. 直面线程安全
    1. 保证原子性
    2. 保证顺序性
    3. 保证可见性
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值