javaEE初阶--多线程基础二

文章目录

    • synchronized 关键字
    • Java 标准库中的类
    • volatile 关键字
    • wait 和 notify
    • 多线程的经典案例
      • 案例一
      • 案例二(阻塞队列)
      • 案例三(定时器)
      • 案例四(线程池)

synchronized 关键字

1) 互斥
● synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
● 同一个对象 synchronized 就会阻塞等待.
● 进入 synchronized 修饰的代码块, 相当于 加锁
● 退出 synchronized 修饰的代码块, 相当于 解锁

代码示例:

synchronized void increase(){
count++;
}

如果修饰的普通方法,就相当于把锁对象指定为this.

2)可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
直观来讲,同一个线程针对同一个锁,如果出现了死锁,就是不可重入,如果不会死锁,就是可重入的;

代码示例:
在下面的代码中, increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的. 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释放, 相当于连续加两次锁)这个代码是完全没问题的. 因为 synchronized 是可重入锁.

static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
    synchronized void increase2() {
        increase();
   }
}

●如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
●解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到

代码示例:

synchronized void incerase(){
    synchronized{
    count++
    }
 }

●外层锁: 进入方法,刚开始加锁,这次能够加锁成功,当前锁是没有占用的;
●内层锁: 进入代码块,刚开始加锁,这次加锁不能成功,因为锁被外层占用着,等外层锁释放之后,内层锁才能加锁成功;
●外层锁要执行完整个方法才释放,但要执行完整个方法, 旧的让内层锁继续往下走,因此就导致了死锁;所以可重入锁的意义就是降低了程序员的负担;

3 )死锁的其他场景

1.一个线程一把锁.
2.两个线程两把锁.
3.n个线程m把锁.

4 )死锁的四个必要条件
1.互斥作用,一个锁被一个线程占用以后,其他线程占用不了(所的本质,保证原子性);
2.不可抢占,一个锁被一个线程占用之,其天线程不能把这个锁抢走;
3.请求和保持,当一个线程占据了多把锁,除非显示的释放锁,否则这些锁始终是被该线程持有的
4.环路等待,等待关系成环了.

Java 标准库中的类

1 . 线程不安全的类
● ArrayList
● LinkedList
● HashMap
● TreeMap
● HashSet
● TreeSet
● StringBuilder

2 .线程安全的类
● Vector (不推荐使用)
● HashTable (不推荐使用)
● ConcurrentHashMap
● StringBuffer

volatile 关键字

volatile 修饰的变量, 能够保证 “内存可见性”.

代码在写入 volatile 修饰的变量的时候:
● 改变线程工作内存中volatile变量副本的值
● 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候:
● 从主内存中读取volatile变量的最新值到线程的工作内存中
● 从工作内存中读取volatile变量的副本

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.

代码示例:

static class Counter {
    volatile public int count = 0;
    void increase() {
        count++;
   }
}
public static void main(String[] args) throws InterruptedException {
    final Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.count);
}

wait 和 notify

wait和notify都是Object对象的方,调用wait方法的线程,就会陷入阻塞,阻塞到有其他线程同通过notify来通知.
代码示例:

public class Demo17 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        System.out.println("wait 前");
        object.wait();
        System.out.println("wait 后");
    }
}

wait内部会做的三件事:

1.先释放锁
2.等待其他线程通知.
3.收到通知后,重新获取锁,并继续执行.

因此,想要使用wait 或 notify,旧的搭配synchronized关键字.

代码示例:

public class Demo18 {
    private static Object locker = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            //进行wait
            synchronized (locker){
                System.out.println("wait之前");
                try{
                    locker.wait();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(()->{
            //进行notify
            synchronized (locker) {
                System.out.println("notify之前");
                locker.notify();
                System.out.println("notify 之后");
            }
        });
        t2.start();
    }
}

wait 和 notify都是针对同一个对象来操作的,例如有十个对象m,都调用了m.wait,此时十个线程都是阻塞状态.如果调用了notify,就会把十个线程都唤醒,如果是notifyall,就会把所有线程都唤醒.

多线程的经典案例

案例一

1.饿汉模式(线程安全)

import java.security.Signature;

//通过Singleton 这个类来实现单例模式. 保证Singleton 这个类只有唯一实例
//饿汉模式
class  Singleton {
    //1.使用static创建一个实例, 并且立即进行实例化.
    //  这个instance 对应的实例,就是该类的唯一实例.
    private static Singleton instance = new Singleton();
    //2.为了防止程序员在其他地方不小心new 这个Singleton ,就可以把方法设置为private;
    private Singleton(){}
    //3.提供一个构造方法,让外面能够拿到唯一实例
    public static Singleton getInstance(){
        return instance;
    }
}

public class Demo19 {
    public static void main(String[] args) {
        Singleton instance  = Singleton.getInstance();
        //Singleton instance2 = new Singleton();
    }
}

2.懒汉模式(线程不安全)

//懒汉模式
class Singleton2{
    //1.就不是立即初始化实例
    private static Singleton2 instance  = null;
    //2.巴公祖奥方法设为private
    private Singleton2(){}
    //3.提供一个构造方法来获得上述单例的实例
    //  只有当真正需要这个实例的时候,才会真正去创建这个实例
    public static Singleton2 getInstance(){
        //如果这个条件成立,说明当前的单例未初始化过的 存在线程安全风险 就需要加锁
        if (instance == null) {
            synchronized (Singleton2.class){
                if(instance == null){
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }
}

public class Demo20 {
    public static void main(String[] args) {
        Singleton2 instance = Singleton2.getInstance();
    }
}

案例二(阻塞队列)

1.什么是阻塞队列:
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则. 阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
●当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
●当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.

2.产生阻塞效果
1). 如果队列为空,尝试出队列,就会出现阻塞,阻塞到队列不为空为止;
2). 如果队列为满,尝试入队列,也会出现阻塞,阻塞到队列不为满为止;

3.生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.

代码实现:

import java.beans.IntrospectionException;

class MyBlockingQueue{
    //保存数据的本体
    private int[] data = new int[1000];
    //有效元素个数
    private int size = 0;
    //队首下标
    private int head = 0;
    //队尾下标
    private int tail = 0;

    //专门的锁对象
    private Object locker = new Object();
    //入队列
    public void put(int value) throws InterruptedException {
        synchronized (locker){
            if(size == data.length){
                //队列满了,暂时直接返回
                //return;
                locker.wait();
            }
            //把新的元素放到tail 位置上
            data[tail] = value;
            tail++;
            //处理tail 到达数组末尾的情况
            if (tail >= data.length) {
                tail = 0;
            }
            //tail = tail % data.length;
            size++;  //千万别忘了 插入之后要修改元素个数
            //如果入队列成功 则队列非空.于是就唤醒 take 中的阻塞等待.
            locker.notify();
        }
    }


    //出队列
    public Integer take() throws InterruptedException {
        synchronized (locker){
            if (size == 0){
                //如果队列为空 就返回一个非法值
                //return null;
                locker.wait();
            }
            //取出head 位置的元素
            int ret = data[head];
            head++;
            if (head >= data.length) {
                head = 0;
            }
            size--;
            locker.notify();
            return ret;
        }
    }

}
public class Demo22 {
    private static MyBlockingQueue queue = new MyBlockingQueue();
    public static void main(String[] args) {
        //实现一个简单的生产者消费者模型
        Thread  producer = new Thread(()->{
            int num = 0;
            while (true) {
                try{
                    System.out.println("生产了: " + num);
                    queue.put(num);
                    num++;
                    //当生产者生产的慢一些的时候,消费者旧的跟着上产者的脚步走
                    //Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();
        Thread customer = new Thread(()->{
            while (true) {
                try {
                    int num = queue.take();
                    System.out.println("消费了: " + num);
                    Thread.sleep(500);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        customer.start();
     }
 }

以上是一个生产者消费者模型的多线程代码,put和take里面的每一行代码都是在操作公共的变量,既然如此,直接给整个方法加锁即可(加synchronized).然后实现阻塞效果,使用wait和notify机制.针对那个对象 加锁就是用哪个对象wait,put中的take由take来唤醒,只要take成功了一个元素,队列就不满了,就可以进行唤醒了.

案例三(定时器)

定时器是什么:
定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.

标准库中的定时器:
标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).

代码示例:

Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("hello");
   }
}, 3000);

管理任务:
1). 描述任务
●创建一个专门的类来表示一个定时器中的任务(TimerTask)
2). 组织任务
●通过一定的数据结构来组织.
3). 执行时间到了的任务.
●需要执行时间最靠前的任务,需要一个线程.不停的去检查当前优先队列的队首元素,看看最靠前的这个任务是不是时间到了.

完整代码:


import java.util.concurrent.PriorityBlockingQueue;

//创建一个类,表示一个任务
class MyTask implements Comparable<MyTask>{
    //任务具体要干啥
    private Runnable runnable;
    //任务具体啥时候干,保存任务要执行的毫秒级时间戳
    private long time;

    //after 是一个时间间隔,不是绝对时间戳的值
    public MyTask(Runnable runnable,long delay){
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }
    public void run(){
        runnable.run();
    }

    public long getTime(){
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        //到底是谁见谁,才是一个时间小的在前?需要背下来
        return (int) (this.time - o.time);
    }
}
class MyTimer {
    //定时器内部要能够存放多个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public void schedule(Runnable runnable, long delay) {
        MyTask task = new MyTask(runnable, delay);
        queue.put(task);
        //每次任务插入成功之后,都唤醒一下扫描线程,让线程重新检查一下队首的任务看是否时间到要执行~~
        synchronized (locker) {
            locker.notify();
        }
    }

    private Object locker = new Object();

    public MyTimer() {
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    //先取出队首元素
                    MyTask task = queue.take();
                    //在比较一下看看当前任务时间到了没有
                    long curTime = System.currentTimeMillis();
                    if (curTime < (task).getTime()) {
                        //时间没到,把任务赛回到队列中
                        queue.put(task);
                        //指定一个等待时间
                        synchronized (locker) {
                            locker.wait(task.getTime() - curTime);
                        }
                    } else {
                        //时间到了, 执行这个任务
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}


public class Demo24 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello timer");
            }
        },3000);
        System.out.println("main");
    }
}

案例四(线程池)

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool {
    // 1. 描述任务, 直接使用 Runnable
    // 2. 组织任务, 使用一个阻塞队列来存放若干个任务.
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    // 3. 还需要描述一个工作线程是啥样的
    static class Worker extends Thread {
        // 当前这里的 Worker 线程有好几个~~ , 这些线程要共享同一个任务队列.
        // 通过这里的构造方法, 把咱们上面创建好的任务队列给传到线程里面, 方便线程去取任务.
        private BlockingQueue<Runnable> queue = null;

        public Worker(BlockingQueue<Runnable> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            // 一个线程要做的工作!
            // 反复的从队列中读取任务, 然后执行任务
            while (true) {
                try {
                    // 如果任务队列中不为空, 此时就能立即取出一个任务并执行.
                    // 如果任务队列为空, 就会产生阻塞, 阻塞到有人加入新的任务为止.
                    Runnable task = queue.take();
                    task.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 4. 需要组织若干个工作线程.
    private List<Worker> workerList = new ArrayList<>();

    // 5. 搞一个构造方法, 指定一下有多少个线程在线程池中.
    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Worker worker = new Worker(queue);
            // 创建好的线程, 先让它跑起来, 再保存到数组中.
            worker.start();
            workerList.add(worker);
        }
    }

    // 6. 实现一个 submit 来注册任务到线程池中.
    public void submit(Runnable runnable) {
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Demo26 {
    public static void main(String[] args) {
        MyThreadPool pool = new MyThreadPool(10);
        for (int i = 0; i < 100; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
    }
}

线程池内容:

●能够描述任务(直接使用Runnable).
●需要组织任务
●能够描述工作线程
●还需要组织这些线程
●实现往线程路里面添加任务

你可能感兴趣的:(笔记,教程,java-ee,java,开发语言)