java多线程

目录太长了,体验不好,就看左边的目录吧,自动生成的目录有点水。。

多线程概要

什么是进程?

在冯诺依曼体系下,整个计算机设备分为,应用程序,操作系统,处理器(cpu),主存,I/O设备。应用程序在操作系统的调节下在处理器上进行合理的资源分配,而在其中一个运行起来的程序,就是进程。

cpu有一个概念,核心数和线程,核心为物理核心,线程为逻辑核心

而进程管理其实分为两步:

        1.描述一个进程:使用结构体或类,把一个进程有哪些信息,表示出来。

        2. 组织这些进程:使用一定的数据结构,把这些结构体/对象,放在一起。

进程的特点:
  1.   PID :每个进程需要有一个唯一的身份标识
  2.   内存指针:当前这个进程使用的内存是哪一部分,进程一旦开启,就会消耗一定的硬件资源
  3.  文件描述符:进程每次打卡一个文件,就会产生一个“文件描述符”  ,被标识了的意味着这个文件已打开。而一个进程会打开多个文件,然后呢就会把这些文件描述符放到循序表中,构成文件描述符表
  4. 进程调度:
  • 进程状态:就绪态,阻塞态,前者表示该进程已准备好,可以随时上cpu上执行,后者还需等待
  • 进程优先级:那个进程优先级高就先执行那个进程
  • 进程的上下文:就是描述了当前进程执行到哪里这样的“存档记录”,进程在离开CPU的时候就要把当前运行的中间结果存档在cpu的寄存器中,等到下次进程回来CPU上,在恢复之前的存档,从上次结果开始
  • 进程的记账信息:统计了每个进程,在CPU上执行了多久,可以作为调度的参考依据。
  • 并发和并行:指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。它们虽然都说是"多个进程同时运行",但是它们的"同时"不是一个概念。并行的"同时"是同一时刻可以多个进程在运行(处于running),并发的"同时"是经过上下文快速切换,使得看上去多个进程同时都在运行的现象,是一种OS欺骗用户的现象

内存分配:

        操作系统给进程分配的内存,是以“虚拟地址空间”的方式进行分配。

什么是多线程

之前说过,进程是一个运行程序,然而一个程序内的功能有很多个,而这其中就有一个问题,就是客户可能会同时用一个程序的多个功能。诺是按照以前我们的写发就是一个main方法,去实现一个主要功能,肯定是不行的。为了应对这个情况,多线程运行就在所难免。

需求决定技术发展

线程是更轻量的的进程。约定一个进程可以包含多个线程,此时多个线程每个线程都是一个独立可以调度执行的执行流(并发),这些线程公用同一份进程的系统资源。

  1. 创建线程比创建进程更快.
  2. 销毁线程比销毁进程更快.
  3. 调度线程比调度进程更快.

可以理解为,一个工厂(进程),中有很多个生产线:(线程)(调用同一份资源,内存空间,文件描述符)。

其中几个问题要重点理解。一个厂子也就意味着资源和场地是一定的,如果为了生产效率,盲目去增加生产线,不去顾忌这些,反而会使的整个生成效率变慢。同理一个主机的核心也是有限,所以增加的线程数和进程数也是有限度。而一台主机到限度了,就可以增加另一台主机,从而使得核心数增加(也就是分布式处理)。

进程和线程的区别:

1.进程包含线程

2.进程有自己独立的内存空间和文件描述符,同一个进程的多个线程之间,共享同一份地址空间和文件描述符

3.进程是操作系统资源分配的基本单位,线程是操作系统调度的基本单位

4.进程之间具有独立性,一个进程挂了,不会影响到别的进程;同一个进程里的多个线程之间,一线程挂了,可能会把整个进程带走,会影响到其他线程的。

多线程编程:

Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装。
操作系统内核实现了线程这样的机制 , 并且对用户层提供了一些 API 供用户使用( 例如 Linux pthread )

这是我们第一个多线程。

public class ThreadDemo {
    private static class MyThread extends Thread {
        @Override
        public void run() {
            Random random = new Random();
            while (true) {
                // 打印线程名称
                System.out.println(Thread.currentThread().getName());
                try {
                    // 随机停止运行 0-9 秒
 Thread.sleep(random.nextInt(10));
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
       }
   }
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
        Random random = new Random();
        while (true) {
            // 打印线程名称
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(random.nextInt(10));
           } catch (InterruptedException e) {
                // 随机停止运行 0-9 秒
                e.printStackTrace();
           }
       }
   }
}
 创建线程
1.继承 Thread
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
   }
}

  创建一个线程实例

MyThread t = new MyThread();

   调用 start 方法启动线程

t.start(); // 线程开始运行
2.实现 Runnable 接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
   }
}

创建 Thread 类实例 , 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数
Thread t = new Thread(new MyRunnable());

调用start方法

t.start(); // 线程开始运行
class MyRunnabble implements Runnable{

    @Override
    public void run() {
        while (true)
        {
            System.out.println("123__true");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class test1 {
    public static void main(String[] args) {
        MyRunnabble runnabble=new MyRunnabble();
        Thread t=new Thread(runnabble);
        t.start();
        while (true) {
            System.out.println("main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


}
  1. 继承 Thread , 直接使用 this 就表示当前线程对象的引用.
  2. 实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用 Thread.currentThread()

常见方法:

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名

常见属性:

// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread () {
    @Override
    public void run () {
        System . out . println ( " 使用匿名类创建 Thread 子类对象 " );
  }
};
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread ( new Runnable () {
    @Override
    public void run () {
        System . out . println ( " 使用匿名类创建 Runnable 子类对象 " );
  }
});
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t4 = new Thread (() -> {
    System . out . println ( " 使用匿名类创建 Thread 子类对象 " );
});
多线程的优势
1. 增加运行速度,通过记录时间戳
        使用 System.nanoTime() 可以记录当前系统的纳秒级时间戳 .
        
public class test3 {
     static int count=0;
    public synchronized static void sum(){
        count++;
    }
    public static void main(String[] args) throws InterruptedException {
        long time=System.currentTimeMillis();
        Thread t1=new Thread(()->{
                for (int i = 0; i < 10000; i++) {
                    sum();
                }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                sum();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
        System.out.println(System.currentTimeMillis()-time);
    }
}
  1. ID 是线程的唯一标识,不同线程不会重复
  2. 名称是各种调试工具用到
  3. 状态表示线程当前所处的一个情况,下面我们会进一步说明
  4. 优先级高的线程理论上来说更容易被调度到
  5. 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
  6. 是否存活,即简单的理解,为 run 方法是否运行结束了
  7. 线程的中断问题,下面我们进一步说明
中断问题:
一个程序要执行很多次,可是我们突然有了一个需求要添加,就得让这个程序停下来,所以我就需要线程中断。
目前常见的有以下两种方式:
1. 通过共享的标记来进行沟通
注意变量捕获,但是java中要求变量捕获,捕获的变量的必须要final或者“实际final”及没有用final修饰,但是代码中没有做出修改。
public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        public volatile boolean isQuit = false;
        @Override
        public void run() {
            while (!isQuit) {
                System.out.println(Thread.currentThread().getName()
                        + ": 转账");
                try {
                    Thread.sleep(1000);
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
           
       }
   }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()
                + ": 让李四开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 通知李四对方是个骗子!");
        target.isQuit = true;
   }
}
2. 调用 interrupt() 方法来通知

使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定
义标志位 .
public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            // 两种方法均可以
            while (!Thread.interrupted()) {
            //while (!Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread().getName()
                        + ": 转账!");
                try {
                    Thread.sleep(1000);
               } catch (InterruptedException e) {
                    e.printStackTrace();
       
                    break;
               }
           }

       }
   }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()
                + ": 开始转账。");
        thread.start();
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName()
                + ": 对方是个骗子!");
        thread.interrupt();
   }
}
方法说明
public void interrupt()
中断对象关联的线程,如果线程正在阻塞,则以异常方式通知, 否则设置标志位
public static boolean
interrupted()
判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean
isInterrupted()
判断对象关联的线程的标志位是否设置,调用后不清除标志位
thread 收到通知的方式有两种(这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到):
1.  如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通
知, 清除中断标志
  • 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
2.  否则,只是内部的一个中断标志被设置, thread 可以通过
  • Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
  • Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
注意: interrupt():作用设置标志位true,如果该线程在阻塞中,此时会把阻塞状态唤醒,抛出异常的方式中断。(当sleep(也可看做阻塞)被唤醒会自动把interrupted,将标志位清空)
产生阻塞的方法,会使得看到标识位为true,任然会抛出异常和清空标志,如果设置 interrupt的时候,阻塞巧合醒了,这个时候程序执行到下一个循环判断条件就结束了。
等待一个线程-join()
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。这样可以让线程变得可控。线程的调度是随机的,无法判定两个线程谁先结束,谁先开始。而jion就是确定谁先开始的方法。
方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos)同理,但可以更高精度
public class test2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(){
            @Override
            public void run() {
                while (true) {

                    System.out.println("123545_run");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                    }
                }
            }
        };
        t.start();
        t.join();
        while (true) {

            System.out.println("123545_run");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
NEW: 安排了工作 , 还未开始行动
RUNNABLE: 可工作的 . 又可以分成正在工作中和即将开始工作 .
BLOCKED: 这几个都表示排队等着其他事情
WAITING: 这几个都表示排队等着其他事情
TIMED_WAITING: 这几个都表示排队等着其他事情
TERMINATED: 工作完成了 .
public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
       }
   }
}

BLOCKED 表示等待获取锁 , WAITING TIMED_WAITING 表示等待其他线程发来通知 .
TIMED_WAITING 线程在等待唤醒,但设置了时限 ; WAITING 线程在无限等待唤醒

线程安全(风险)

某个代码在多线程的环境下执行,然后出现bug,其本质原因在于线程调度是不确定的。
比如:(代码有问题)
public class test3 {
     static int count=0;
    public   static void sum(){
        count++;
    }
    public static void main(String[] args) throws InterruptedException {
        long time= System.nanoTime();
        Thread t1=new Thread(()->{
                for (int i = 0; i < 10000; i++) {
                    sum();
                }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                sum();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);

}

我们会发现这个代码出现了一个问题,与我们想要的预期不一致。即出现bug

其本质是,count++操作,本质是有三个CPU指令构成

1.load,把内存中的数据读到cpu寄存器中。

2.add,就是把寄存器中的值,进行+1操作

3.save,把寄存器中值写回内存中。

大家肯定学过数学,那么对于组合,肯定是有过了解的。那么我问个问题,现在线程有两个,分别对count进行++操作。对于cpu来说有几种组合方式?3*3共有9种,那么问题来了,我们只要唯一的结果,不需要这么结果可能。

这个是我随便选择的两种情况,画出来的。箭头向下,表示时间的执行顺序。可以看到,第一个,t1的load执行后,t2的load开始执行了,然后执行t2的add操作,和save操作,再然后才执行t1的add操作和save操作。所以出现了不同的结果。
  • 线程之间的共享变量存在 主内存 (Main Memory).
  • 每一个线程都有自己的 "工作内存" (Working Memory) .
  • 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  • 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

 其实在进一步理解,可以理解为,两个线程对同一个变量,进行了相互作用。

线程不安全的原因:

1.抢占式执行(大部分的原因)

2.多个线程修改同一个变量(不安全)(而有几种情况是安全:

    一个线程改同一个变量(安全),多个线程读同一个变量(安全),多个线程修改不同变量)

3.修改操作,不是原子性的。

4.内存可见性,引起的线程不安全。

5.指令重排序,引起的线程不安全。

那么如何是的让++操作不会被干扰呢?
解决线程不安全:
主要思路是:诺是我们有一种操作,将++操作封装起来,让t1的++操作结束,再让t2的++操作开始。而这种操作就是加锁操作。
简单理解就是,目前有一个厕所,但是有很多要上厕所,怎么办,抢呗,总不可能等着膀胱爆炸吧,等待厕所里的人出来,并且打开厕所门,然后一群人看谁快,谁快谁就先如厕。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的
synchronized
java中提供了一个关键字: synchronized,监视器锁 monitor lock

synchronized 会起到互斥效果 , 某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也执行到 同一个对象 synchronized 就会 阻塞等待 .
  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁
synchronized 用的锁是存在 Java对象头里的, 底层是使用操作系统的 mutex lock 实现的 .
内存刷新
工作流程:
synchronized 的工作过程 :
1. 获得互斥锁
2. 从主内存拷贝变量的最新副本到工作的内存
3. 执行代码
4. 将更改后的共享变量的值刷新到主内存
5. 释放互斥锁
可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题
这个问题呢,在synchronized中不会出现。而可重入其实就是,有人厕所上完了,但是他很缺德,将门锁了,然后呢,他突然发现他包忘哪了,回去后他在厕所门口等着,结果等了半天,里面的人死活不出来。但是厕所里没人。
static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
    synchronized void increase2() {
        increase();
   }
}
  • increase increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.
  • 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释, 相当于连续加两次锁)

可重入锁的内部, 包含了 "线程持有者" "计数器" 两个信息:

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

synchronized 使用:

  • 直接修饰普通方法: 锁的 SynchronizedDemo 对象
public class SynchronizedDemo {
    public synchronized void methond() {
   }
}
  • 修饰静态方法: 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
    public synchronized static void method() {
   }
}
  • 修饰代码块: 明确指定锁哪个对象.
锁当前对象)
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            
       }
   }
}
(锁类对象 )
public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}
两个线程竞争同一把锁 , 才会产生阻塞等待
Java 标准库中很多都是线程不安全的 . 这些类可能会涉及到多线程修改共享数据 , 又没有任何加锁措施 .
  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder
还有一些是线程安全的 . 使用了一些锁机制来控制 .
  • Vector (不推荐使用)
  • HashTable (不推荐使用)
  • ConcurrentHashMap
  • StringBuffer
volatile 关键字
volatile 能保证内存可见性

1.代码在写入 volatile 修饰的变量的时候 ,
  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存
2.代码在读取 volatile 修饰的变量的时候 ,
  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

static class Counter {
    public int flag = 0;
}
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (counter.flag == 0) {
            // do nothing
       }
        System.out.println("循环结束!");
   });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        counter.flag = scanner.nextInt();
   });
    t1.start();
    t2.start();
}

根据上面的代码,发现问题没有,无论我们在控制台中输入什么值,程序都不会结束。为什么呢,就像上面所说的那样,一个cup的寄存器中数据并没进行更新。另一个线程所拿到的数据没有进行跟换。其主要原因是计算机运算速度太快了。寄存器和缓存的速度都太快了,

使用特点:

1.volatile 不保证原子性

2.volatile 适用于一个线程读,一个线程写

3.synchronized 既能保证原子性, 也能保证内存可见性.

改正后:

static class Counter {
  volatile  public int flag = 0;
}
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (counter.flag == 0) {
            // do nothing
       }
        System.out.println("循环结束!");
   });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        counter.flag = scanner.nextInt();
   });
    t1.start();
    t2.start();
}

此时就可以结束进程了。

wait notify
由于线程之间是抢占式执行的 , 因此线程之间执行的先后顺序难以预知 . 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.
完成这个协调工作 , 主要涉及到三个方法
  • wait() / wait(long timeout): 让当前线程进入等待状态.
  • notify() / notifyAll(): 唤醒在当前对象上等待的线程.
wait()
wait 做的事情 :
  1. 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  2. 释放当前的锁
  3. 满足一定条件时被唤醒, 重新尝试获取这个锁.
wait 结束等待的条件 :
  1. 其他线程调用该对象的 notify 方法.
  2. wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  3. 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
notify ()
notify 方法是唤醒等待的线程 .
  1. 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的
  2. 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  3. 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
  4. notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行
  5. 完,也就是退出同步代码块之后才会释放对象锁。

wait和notify的使用(只能一个结束,一个开始)

static class WaitTask implements Runnable {
    private Object locker;
    public WaitTask(Object locker) {
        this.locker = locker;
   }
    @Override
    public void run() {
        synchronized (locker) {
            while (true) {
                try {
                    System.out.println("wait 开始");
                    locker.wait();
                    System.out.println("wait 结束");
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
       }
   }
}
static class NotifyTask implements Runnable {
    private Object locker;
    public NotifyTask(Object locker) {
        this.locker = locker;
   }
    @Override
    public void run() {
        synchronized (locker) {
            System.out.println("notify 开始");
            locker.notify();
            System.out.println("notify 结束");
       }
   }
}
public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();
    Thread t1 = new Thread(new WaitTask(locker));
    Thread t2 = new Thread(new NotifyTask(locker));
    t1.start();
    Thread.sleep(1000);
    t2.start();
}
notify 方法只是唤醒某一个等待线程 .。 使用 notifyAll 方法可以一次唤醒所有的等待线程。 虽然是同时唤醒多个 线程 , 但是这些 线程需要竞争锁。   所以并不是同时执行 , 而仍然是有先有后的 执行。
wait与sleep的区别:
其实理论上 wait sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间。
1. wait 需要搭配 synchronized 使用 .,sleep 不需要。
2. wait Object 的方法 sleep Thread 的静态方法。

多线程模式:

软件开发中也有很多常见的 "问题场景". 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照
这个套路来实现代码, 也不会吃亏。大佬们为我们操碎了心。
单例模型(某一个类,在进程中只有唯一一个实例)
分为: 饿汉模式 懒汉模式
饿汉模式:就是将文件所有的内容都读到内存中,并显示。(小规模就好,太多了内存不够,所以懒汉模式)
class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
   }
}
被static修饰,该属性是类的属性(类对象上),JVM中,每个类的对象只有唯一一份,类对象的成员自然也是唯一一份。
private将new操作给静止掉,在类内部把实例创建好同事静止外部重新创建实例,此时,就可以保证单例的特性。
懒汉模式:只读取文件的一小部分。把当前屏幕填充上,如果用户翻页了,再读其他文件内容,如果不翻页,就可以节约运算资源。
单线程:核心思想,非必要,不创建。 第一次使用的时候才创建实例
class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
       }
        return instance;
   }
}
多线程:
上面的代码是不安全的,为什么?因为在多线程下, 同时调用 getInstance 方法, 就可能导致
创建出多个实例。
加上 synchronized 可以改善这里的线程安全问题
class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
       }
        return instance;
   }
}

加锁进行实例化对象,是很耗资源。其实一个实例创建后,在内存中已经存在,其他线程其实更多的是读操作。那么就没必要去进行加锁操作。

对上述代码改进:

class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        //判断是否为空,诺不为空,就不需进行加锁实例化。
        if (instance == null) {
            synchronized (Singleton.class) {
                //进行实例化判断
           if (instance == null) {
               instance = new Singleton();
               }
           }
       }
        return instance;
   }
}

两个if所代表的意义有所不同。

1.加锁if:把if和new变为原子操作

2.双重 if:减少不必要的加锁操作

3.使用volatile 禁止指令重排序,保证后续线程肯定拿到的是完整对象。

单例模式:线程安全问题:

饿汉模式:天然就是安全的,只是读操作

懒汉模式:不安全的,有读也有写。

阻塞队列(线程安全)

本质是一个循环队列,但是它带有阻塞特性

1.如果入队列为空,尝试出队列,就会阻塞等待。等待到队列不空为止。

2.如果队列满了,尝试入队,就会阻塞等待,等待到队列不满为止。

这个就有一个经典的模型进行解释--生产者消费者模型,什么是生产者消费者模型?其实简单理解为,生产效率与消费效率的比值。比如一个面包厂1小时生产4个面包,而此时有很多人等着吃面包。这个就是简单的生产者消费者模型。

这个数据结构的模式有几个好处:

1.可以让上下游模块之间,可以更好的“解耦和”

队列与具体业务无关,队列中的某一个线程挂了,不影响其他线程,比如电脑有时候网页会卡,但是某些功能还在运行。

2.削峰填谷

不知道各位有没有打游戏,王者农药肯定都听过,其中有个事情,在游戏早期,它出了一款皮肤,这个皮肤很受玩家喜欢,再上线的那一刻,众多玩家,蹲点购买。使得当时的支付系统蹦了几分钟。为了应对这种情况,阻塞队列就可以减少这种风险。

在Java中提供了一个阻塞队列的数据的集合,BlockingQueue

  • BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
  • put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
  • BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞. 
String elem = queue.take();

重点是如何自己去实现这种数据结构:

主要分为三步:

1.先实现一个普通队列

2.加上线程安全

3.加上阻塞功能

class MyBlockingQueue{
    //普通队列
    private int [] items=new int[1000];
    //规定head--tail的范围为有效范围
  volatile   private int head=0;
  volatile private int tail=0;
  volatile   private int size=0;
    //入队列
  synchronized   public void put(int elem) throws InterruptedException {
        //队列元素满了
        while (size==items.length){
            this.wait();
        }
        items[tail]=elem;
        tail++;
        //判断是否到达末尾,队列中的元素没有满的情况下
        if (tail==items.length){
            tail=0;
        }
      //可读性下差,开发效率慢
      //tail=tail%items.length;
        this.notify();
      size++;
    }
    //出队列
   synchronized public Integer take() throws InterruptedException {
        while (size==0){
            this.wait();
        }
        int value=items[head];
        head++;
        if (head==items.length){
            head=0;
        }
        this.notify();

       size--;
       return value;
    }
}

public class test8 {
    public static void main(String[] args) {
        MyBlockingQueue queue=new MyBlockingQueue();

        Thread t1=new Thread(()->{

            while (true){
                try {
                   int value= queue.take();
                   System.out.println("消费:"+value);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        Thread t2=new Thread(()->{
            int value=0;
            while (true) {
                try {
                    System.out.println("生产:"+value);
                    queue.put(value);
                    Thread.sleep(1000);
                    value++;
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });



    }
}

入队成功后其他线程才能出队,出队成功后其他线程才能入队。

定时器:

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

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

Timer timer = new Timer ();
timer . schedule ( new TimerTask () {
    @Override
    public void run () {
        System . out . println ( "hello" );
  }
}, 3000 );
实现一个定时器:
定时器的构成 :
  1. 一个带优先级的阻塞队列(阻塞队列中的任务都有各自的执行时刻 delay. 最先执行的任务一定是 delay 最小的. 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来.)
  2. 队列中的每个元素是一个 Task 对象.
  3. Task 中带有一个时间属性, 队首元素就是即将运行的。
  4. 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行
class MyTask implements Comparable<MyTask>{
    public Runnable runnable;
    //为了方便后续,使用绝对的时间戳
    public long time;

    public MyTask(Runnable runnable,long delay){
        this.runnable=runnable;
        //获取当前时刻的时间戳+delay,作为任务的实际执行时间
        this.time=System.currentTimeMillis()+delay;
    }
    @Override
    public int compareTo(MyTask o) {
        //设置比较器,构建优先级队列
        return (int)(this.time-o.time);
    }
}

class MyTimer{
    //这个结构,带有优先级的阻塞对列,核心数据结构
    //创建一个锁对象
    private Object loker=new Object();
    private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();


    //此处的dalay 是一个形如3000这样的数字(多长时间后执行)
    public void schedule(Runnable runnable,long dalay){
        //根据参数,构造MyTask,插入队列即可
        MyTask myTask=new MyTask(runnable,dalay);
        queue.put(myTask);
        synchronized (loker){
            loker.notify();
        }
    }
    //构造线程
    public MyTimer(){
        Thread t=new Thread(()->{
            while (true) {
                try {
                    synchronized (loker){
                        MyTask myTask=queue.take();
                        long curTime=System.currentTimeMillis();
                        if (myTask.time <= curTime){
                            //时间到了,执行任务
                            myTask.runnable.run();
                        }else {
                            //时间还没到
                            //将刚刚取出的任务,重新塞回队列
                            queue.put(myTask);
                           loker.wait(myTask.time-curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
    }
}

public class test10 {
}

线程池:

(就是就是装有很多线程的仓库,使用线程从里面拿就好)

线程池最大的好处就是减少每次启动、销毁线程的损耗。
 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池 .
返回值类型为 ExecutorService 通过 ExecutorService.submit 可以注册一个任务到线程池中 .
submit放入线程
ExecutorService pool = Executors . newFixedThreadPool ( 10 );
pool . submit ( new Runnable () {
    @Override
    public void run () {
        System . out . println ( "hello" );
  }
});
源代码:

 

  • corePoolSize:核心线程数(不会消失)
  • maximumPoolSize:最大线程数(核心线程数+零时线程)
  • keepAliveTime: 临时线程允许的空闲时间.
  • unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.
  • workQueue: 传递任务的阻塞队列
  • threadFactory: 创建线程的工厂, 参与具体的创建线程工作.
  • RejectedExecutionHandler: 拒绝策略, 如果任务量超出线程池的负荷了接下来怎么处理:
    1. AbortPolicy(): 超过负荷, 直接抛出异常.
    2. CallerRunsPolicy(): 调用者负责处理
    3. DiscardOldestPolicy(): 丢弃队列中最老的任务.
    4. DiscardPolicy(): 丢弃新来的任务

Executors 创建线程池的4种方式
  • newFixedThreadPool: 创建固定线程数的线程池
  • newCachedThreadPool: 创建线程数目动态增长的线程池.
  • newSingleThreadExecutor: 创建只包含单个线程的线程池.
  • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的Timer.

实现线程池

  1. 核心操作为 submit, 将任务加入线程池中
  2. 使用 Worker 类描述一个工作线程. 使用 Runnable 描述一个任务.
  3. 使用一个 阻塞队列中组织所有的任务
  4. 每个 worker 线程要做的事情: 不停的从 阻塞队列中取任务并执行.
  5. 指定一下线程池中的最大线程数 maxWorkerCount,当前线程数超过这个最大值时, 就不再新增线程了。
class Worker extends Thread {
    private LinkedBlockingQueue<Runnable> queue = null;
    public Worker(LinkedBlockingQueue<Runnable> queue) {
        super("worker");
        this.queue = queue;
   }
    @Override
    public void run() {
        // try 必须放在 while 外头, 或者 while 里头应该影响不大
        try {
            while (!Thread.interrupted()) {
                Runnable runnable = queue.take();
                runnable.run();
           }
       } catch (InterruptedException e) {
       }
   }
}
public class MyThreadPool {
    private int maxWorkerCount = 10;
    private LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue();
    public void submit(Runnable command) {
        if (queue.size() < maxWorkerCount) {
            // 当前 worker 数不足, 就继续创建 worker
            Worker worker = new Worker(queue);
            worker.start();
       }
        // 将任务添加到任务队列中
        queue.put(command);
   }
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool();
        myThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("吃饭");
           }
       });

        Thread.sleep(1000);
   }
}

锁策略

上面我说过,锁是为了解决线程冲突的问题。但是我也说过加锁操作会影响程序的效率。(因为阻塞),为了应对这个我们应该合理去进行加锁操作,那么就应该有策略的操作。

1.乐观锁VS悲观锁

乐观锁:   预测接下来冲突概率不大(做的工作少)--->效率会快一些

悲观锁:预测接下了的冲突概率不大(做的多)--->x效率会慢一些

其实这两个就是预测接下来的锁冲突(阻塞等待)的概率是大,还是不大,根据这个冲突的概率,决定接下来怎么做。

Synchronized 初始使用乐观锁策略 . 当发现锁竞争比较频繁的时候 , 就会自动切换成悲观锁策略 .

2.轻量级锁VS重量级锁

轻量级锁:加锁解锁的过程更快更高效。(一个乐观锁很可能是一个轻量级锁)

重量级锁:加锁解锁,过程更慢,更低效。(一个悲观锁很可能是一个重量级锁)

3.自旋锁VS挂起等待锁

自旋锁:是轻量级锁的一种典型实现(纯用户态的不需要经过内核态(时间相对更短))

加锁失败后,不停等待的去问是否可以加锁了

挂起等待锁:是重量级锁的一种典型实现(通过内核机制来实现挂起等待(时间更长了))

加锁失败后,先去做其他事情,等这个锁给我信号后我就回来加锁。

Synchronized 既是悲观锁,也是乐观锁,既是轻量级锁,也是重量级锁;轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁实现。

Synchronized 会根据当前锁竞争的激烈程度,自适应;

  • 如果冲突不激烈,以轻量级锁或者乐观锁的状态运行
  • 如果激烈,以重量级锁或悲观锁的状态运行。

4.互斥锁VS读写锁

互斥锁:

synchronized是一个互斥锁,就单纯的加锁。通常只有两种操作:

  1. 进入代码块,加锁
  2. 出代码块,解锁

读写锁:

有一种锁,把读操作和写操作分开加锁(线程安全):

  1. 给读加锁
  2. 给写加锁
  3. 解锁

约定:

  1. 读锁和读锁之间,不会锁竞争,不会产生冲突(不会影响程序之间的效率)
  2. 写锁和写锁之间,有锁竞争(减慢速度,保证准确性)
  3. 读锁和写锁之间,有锁竞争(减慢速度,保证准确性)

Java中专门提供了读锁一个类,写锁一个类。

5.可重入锁vs不可重入锁

  • 如果一个锁,在一个线程中,连续对锁,锁了两次,不产生死锁,叫可重入锁。
  • 如果一个锁,在一个线程中,连续对锁,锁了两次,产生死锁,叫不可重入锁。

死锁的第一种情况

如何产生死锁,我们对一个代码加两次锁,此时内部的锁要等待外部的锁释放才能加锁,而此时外部的锁释放,需要等待内部锁加锁成功。然后逻辑上矛盾了,于是产生了死锁。

死锁的第二种情况

两个线程两把锁,即使单个线程是可重入锁,也会死锁。

 线程1的外部锁加锁,需要等待线程2内部锁释放,同理线程2外部锁加锁,需要等待线程1内部锁释放,此时逻辑矛盾,产生死锁。

死锁的第三种情况

哲学家,就餐问题(N个线程,M把锁)

一个桌子上有五只筷子。也有五个人,桌上有一碗面,每个人只能用一双筷子吃一口。诺是五个同时拿起一只筷子,场上就构不成一双筷子的条件,也就是谁都吃不了面。此时就死锁了。

 怎么办,很简单,五个人约定一个规则,谁先吃,谁后吃,此时就可以避开死锁的情况。

死锁的四个必要条件

  • 互斥使用:一个线程拿到一把锁后,另一个线程不能使用(根本问题锁的基本特点)
  • 不可抢占:一个线程拿到锁,只能自己主动释放,不能是被其他线程强行占有
  • 请求和保持:一个线程拿到一个锁,不去做事,反而想拿到第二把锁。
  • 循环等待:逻辑冲突。谁都拿不到。

实践中如何避免死锁?

对锁进行编号,如果需要获取多把锁,就约定加锁顺序,务必先对编号小的加锁,在对编号大的加锁。

公平锁VS非公平锁

约定:

遵循先来后到,就是公平锁,不遵守先来后到的(等概率竞争是不公平的),非公平锁。

synchronized是非公平的,要实现公平就需要在synchronized的基础上,加个队列来记录这些加锁线程的顺序。

总结一下synchronized的特点:

  1. 既是乐观锁,也是悲观锁
  2. 既是轻量级锁,也是重量级锁
  3. 轻量级锁基于自旋锁实现,重量级锁基于挂起等待实现
  4. 不是读写锁
  5. 是可重入锁
  6. 是非公平锁

CAS

CAS: 全称 Compare and swap ,字面意思 :“ 比较并交换” ,一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
1. 比较 A 与 V 是否相等。(比较)
2. 如果比较相等,将 B 写入 V。(交换)
3. 返回操作是否成功。
真实的 CAS (即cpu的一条指令)是一个原子的硬件指令完成的(具有原子性),相当于我们不加锁,就能保证线程安全。
当多个线程同时对某个资源进行 CAS 操作,只能有一个线程操作成功,但是并不会阻塞其他线程 ,其他线程只会收到操作失败的信号。
CAS 可以视为是一种乐观锁. (或者可以理解成 CAS 是乐观锁的一种实现方式)
讲到锁操作的时候,我们说过因为一个读一个写的两个线程,他们不会自己去检查变量是否发生过改变。但是CAS却可以进行自检,并返回是否成功。
基于CAS实现的操作:
1.实现原子类
标准库里提供AtomInteger类保证程序的原子性
  • AtomicBoolean
  • AtomicInteger
  • AtomicIntegerArray
  • AtomicLong
  • AtomicReference
  • AtomicStampedReference
2.实现自旋锁
通过CAS的自检性,反复检查当前的锁状态,看是否解开了

但是CAS不是没有问题,最典型的问题A->B->A问题,其实就是我们要内存改变的值与内存的值一样,是得不断在A--B--A中不断横跳。在具体一点就是,两个线程(t1,t2)对数据进行减法,(t3)还有一个对数据进行加法,而加的数据与减的数据一样。

那么就会有一个问题。两个线程中其中一个线程(t1)提前做了减操作,接下来是(t3)加操作,此时内存的值没变,t2线程发现值是原来的值,又做了一次减操作。(这显然不是我们所期望的)

如何解决呢?

加入一个衡量内存的值是否变化的量,俗称版本号,版本号只能增加无法减少,每一次修改版本+1,这样我只需对比版本号本身就可以避免aba问题。

synchronized的锁策略:锁升级

偏向锁:非必要,不加锁

先让线程针对锁,有个标记,如果整个代码执行过程中没有遇到别的线程和我竞争这个所,我就加锁了。但是如果有人来竞争,就升级为真的锁。这样既保证了效率,也保证了线程安全。

锁消除

基础逻辑是,非必要不加锁。编译器+JVM 判断锁是否可消除如果可以就直接消除。检测当前代码是否多线程执行,判断是否有必要加锁,如果没有必要,但是又加上了锁,就会在编译过程中自动取消掉。

比如StringBuffer,在源码内加入了synchronized关键字。诺是单线程就必要加锁了,也就可以取消掉。

锁粗化

锁的粒度,synchronized代码块,包含代码的多少(代码越多,粒度越粗,越少,粒度越细),多数情况希望锁的粒度更小。(串行代码少,意味着并发代码就多。)

如果有一个场景需要频繁的加锁解锁,此时就会将整个场景锁起来,变成一个更粗的锁

Callable 的用法

Callable 是一个 interface . 相当于把线程封装了一个 " 返回值 ". 方便程序猿借助多线程的方式计算结果, 非常类似于Runnable,只不过返回值不是void,而是泛型
创建线程计算 1 + 2 + 3 + ... + 1000(非callable)
//创建一个类 Result , 包含一个 sum 表示最终结果, lock 表示线程同步使用的锁对象
static class Result {
    public int sum = 0;
    public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {

    Result result = new Result();
    Thread t = new Thread() {
        @Override
        public void run() {
//main 方法中先创建 Result 实例, 然后创建一个线程 t. 在线程内部计算 1 + 2 + 3 + ... + 1000
            int sum = 0;
            for (int i = 1; i <= 1000; i++) {
                sum += i;
           }
            synchronized (result.lock) {
                result.sum = sum;
//主线程同时使用 wait 等待线程 t 计算结束
                result.lock.notify();
           }
       }
   };
    t.start();
    synchronized (result.lock) {
        //
        while (result.sum == 0) {
            result.lock.wait();
       }
//当线程 t 计算完毕后, 通过 notify 唤醒主线程, 主线程再打印结果.
        System.out.println(result.sum);
   }
}

创建线程计算 1 + 2 + 3 + ... + 1000(callable)

Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 1000; i++) {
            sum += i;
       }
        return sum;
   }
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result);
  1. 创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
  2. 重写 Callable call 方法, 完成累加的过程. 直接通过返回值返回计算结果.
  3. callable 实例使用 FutureTask 包装一下.
  4. 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable
  5. call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
  6. 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结 .

Callable中泛型是什么,就返回什么。

Callable 和 Runnable的区别

  1. Callable 和 Runnable 相对, 都是描述一个 "任务",Callable 描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务。
  2. Callable 通常需要搭配 FutureTask 来使用.,FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定。
  3. FutureTask 就可以负责这个等待结果出来的工作。

FutureTask 的理解,其实可以理解为,炖汤,通常炖汤我们将食物放入砂锅中,只需要等待时间过去2-3小时,砂锅就能为我们呈现一锅鲜美的汤。

JUC(ava.util.concurrent

ReentrantLock:可重入互斥锁 . synchronized 定位类似 , 都是用来实现互斥效果 , 保证线程安全
用法 :
  1. lock(): 加锁, 如果获取不到锁就死等.
  2. trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.(加锁失败,不会阻塞,直接返回false,更灵活)
  3. unlock(): 解锁
ReentrantLock  Synchronized 的区别:
  1. synchronized 是一个关键字, JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准 库的一个类, JVM 外实现的(基于 Java 实现).
  2. synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
  3. synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就 放弃.
  4. synchronized 是非公平锁, ReentrantLock 默认是非公平锁,但是提供了公平和非公平两种工作模式. 可以通过构造方法传入一个 true 开启公平锁模式.
  5. 更强大的唤醒机制 . synchronized 是通过 Object wait / notify 实现等待 - 唤醒 . 每次唤醒的是一 个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待 - 唤醒 , 可以更精确控制唤醒某个指定的线程.
如何选择使用哪个锁?
  • 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便。
  • 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等。
  • 如果需要使用公平锁, 使用 ReentrantLock。

原子类

原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个
  • AtomicBoolean
  • AtomicInteger
  • AtomicIntegerArray
  • AtomicLong
  • AtomicReference
  • AtomicStampedReference

信号量 Semaphore

本质是一个计数器,描述了当前“可用资源”的个数

  • P操作,申请资源。计数器-1;
  • V操作,释放资源。计数器+1;

如果计数器为0,就阻塞等待,等待出现资源时,及继续申请等待。

  • 创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源。
  • acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
  • 创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果。
Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        try {
            System.out.println("申请资源");
            semaphore.acquire();
            System.out.println("我获取到资源了");
            Thread.sleep(1000);
            System.out.println("我释放资源了");
            semaphore.release();
       } catch (InterruptedException e) {
            e.printStackTrace();
       }
   }
};
for (int i = 0; i < 20; i++) {
    Thread t = new Thread(runnable);
    t.start();
}

CountDownLatch

同时等待 N 个任务执行结束 .
  • 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成。
  • 每个任务执行完毕, 都调用 latch.countDown() . CountDownLatch 内部的计数器同时自减。
  • 主线程中使用 latch.await(); (暗中计算有几个countDown被调用了)阻塞等待所有任务执行完毕. 相当于计数器为 0 了。
public class Demo {
    public static void main(String[] args) throws Exception {
        CountDownLatch latch = new CountDownLatch(10);
        Runnable r = new Runable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(Math.random() * 10000);
                    latch.countDown();
               } catch (Exception e) {
                    e.printStackTrace();
               }
           }
       };
        for (int i = 0; i < 10; i++) {
            new Thread(r).start();
       }
   // 必须等到 10 人全部回来
        latch.await();
        System.out.println("比赛结束");

   }
}

多线程对集合类的使用

常用的集合类:ArrayList,LinkedList,HashMap,PriorityQueue。。。线程是不安全的。

如果要使用怎么办?

1.可以手动对集合的修改操作加锁。(synchronized 或者 ReentrantLock

2.使用java标准库提供的一些线程安全的版本的集合类。

多线程环境使用 顺序表

ArrayList可用,Vertor代替,但是vertor该有的方法都用synchronized,是很老的集合,实际场景并不适用。
1.Collections.synchronizedList(new ArrayList) ;
  • synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
  • synchronizedList 的关键操作上都带有 synchronized
2.使用 CopyOnWriteArrayList
CopyOnWrite容器即写时复制的容器。
  • 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy, 复制出一个新的容器,然后新的容器里添加元素,
  • 添加完元素之后,再将原容器的引用指向新的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会
添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
优点:
  • 在读多写少的场景下, 性能很高, 不需要加锁竞争.
缺点:
  1. 占用内存较多.
  2.  新写的数据不能被第一时间读取到.

多线程环境使用队列

  1.  ArrayBlockingQueue      基于数组实现的阻塞队列
  2.  LinkedBlockingQueue     基于链表实现的阻塞队列
  3.  PriorityBlockingQueue     基于堆实现的带优先级的阻塞队列
  4.  TransferQueue     最多只包含一个元素的阻塞队列
多线程使用队列:BlockingQueue  

多线程环境使用哈希表

在多线程环境下使用哈希表可以使用 :
  • Hashtable

是线程安全的,给关键方法加上synchronized,颗粒度比较粗。它对整个哈市表加锁,任何的增删查操作,都会触发加锁,也就意味着会有锁竞争。其实没有必要,哈希表是有桶的,修改值是要通过key计算hash值,然后将新元素放到链表上。

两个线程对不同量进行修改,不会产生冲突,但是由于方法上加了锁也就意味着,两个线程同时使用一个方法会阻塞。(所以不建议)

  • ConcurrentHashMap

线程是安全的, ConcurrentHashMap不是只有一把锁了,每个桶也就是链表的头结点作为一把,锁,这样针对不同的链表进行操作是不会产生的所冲突。大部分的加锁操作就没有锁冲突。
其他方面的改进:
更充分的利用了CAS机制--无锁编程
有些操作,比如获取或更新某个元素个数,就可以直接使用CAS完成,不必加锁
优化了扩容策略
对于hashTable来说,如果元素太多们就会涉及扩容,诺元素很多很多,上亿个,那么将原表大部分的元素搬到新的位置上,这个操作非常不流畅。所以呢 ConcurrentHashMap,在此基础上,诺put触发扩容机制,就会一次性创建更大的内存空间,然后搬运一部分,此时就相当于存在两个hash表,此时对表操作,插入是对新表插入,删除是对旧表(看元素在那个表上)删除,查找是新旧表都查。(每一次操作。都会从旧表搬运一部分到新表)
HashtableHashMapConcurrentHashMap 之间的区别
  1. HashMap: 线程不安全. key 允许为 null
  2. Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.
  3. ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用CAS 机制. 优化了扩容方式. key 不允许为 null

Java 多线程是如何实现数据共享的 ?
  1. JVM 把内存分成了这几个区域:
  2. 方法区, 堆区, 栈区, 程序计数器.
  3. 其中堆区这个内存区域是多个线程之间共享的.
  4. 只要把某个数据放到堆内存中, 就可以让多个线程都能访问到。

Java 创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?
创建线程池主要有两种方式:
  • 通过 Executors 工厂类创建. 创建方式比较简单, 但是定制能力有限.
  • 通过 ThreadPoolExecutor 创建. 创建方式比较复杂, 但是定制能力强.
LinkedBlockingQueue 表示线程池的任务队列。 用户通过 submit / execute 向这个任务队列中添
加任务, 再由线程池中的工作线程来执行任务。
​​​​​​​

其他文章接口

 1.String方法(重要,对于操作字符串有巨大的帮助)


文章链接


2.java常用的接口及其方法(包含拷贝,比较,排序,构造器)

文章链接


3.初阶数据结构


 3.1 顺序表:ArrayList

[文章链接]


3.2 链表:LinkedList


[文章链接]

3.3 栈:Stack

[文章链接]


 3.4 队列:Queue


[文章链接]


3.5 二叉树:Tree


[文章链接]


3.6 优先级队列:PriorityQueue(堆排序)


[文章链接]


3.7 Map和Set
HashMap和HashSet,TreeMap和TreeSet


[文章链接]


4. 排序(7种方式)


4.1 插入排序(两种)


4.2  选择排序(两种)


4.3 快速排序


4.4 堆排序
里面有堆排序的实现和逻辑


[文章链接]


 4.5  归并排序


5.多线程


[文章链接]


6.网络编程


7.HTML


8.数据库Mysql


[文章链接]
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值