目录
前言
随着业务量和数据的增加,企业不可避免地会使用多线程的方式处理数据。在 Java 职位的面试中,多线程也是必考的高阶知识点之一。可以说,java多线程是衡量一名 Java 程序员是否资深的关键标准之一。
今天,我们就来学习一下 Java 多线程的概念吧!
概念
线程是被包含在进程中的,一个进程会默认有一个线程,也可以有多个线程
每个线程都是一个“执行流”,可以单独的在CPU上进行调度。同一个进程中的这些线程,共用同一份系统资源(内存+文件)
线程是轻量级的进程,创建线程的开销比创建进程小,销毁线程的开销比销毁进程小
多线程的优势
1、能够充分利用多核CPU,提高效率
2、只需要在创建第一个线程时,申请资源,后续在创建新的线程,都是共用同一份资源(节省了申请资源的开销)
线程与进程的区别
1、进程包含线程;
2、线程比进程更轻量,创建更快,销毁也更快;
3、同一个进程之间的多个线程之间共用一份内存/文件资源,进程和进程之间,则是独立的内存/文件资源;
4、进程是资源分配的基本单位,线程是调度执行的基本单位
多线程的创建
多线程的基本创建方式
1、继承Thread,重写run
class MyThread extends Thread{
@Override
public void run(){
System.out.println("hello thread!");
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread t=new MyThread();
t.start();
}
}
创建一个MyThread实例,继承Thread父类,相当于对操作系统中的县城进行的封装。
run是父类中已经有的方法,run里边的逻辑,就是线程要执行的工作。
MyThread中重写run方法,相当于“安排任务”。
MyThread t=new MyThread(); t.start();创建MyThread实例的过程中,并不会在系统中创建出一个线程,调用start方法时,才会创建出一个新的线程,新的线程就会执行run里边的逻辑,一直到run里边的代码执行完,新的线程就运行结束
在上边代码中,有一个进程,两个线程,main方法对应的主线程,和MyThread新线程,这两个线程是并发执行的关系
2、继承Runnable接口,重写run
class MyRunnable implements Runnable{
@Override
public void run() {
while (true){
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo2 {
public static void main(String[] args) {
MyRunnable myRunnable=new MyRunnable();
Thread t=new Thread(myRunnable);
t.start();
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
把线程要干的活和线程本身分开,Runnable表示线程要完成的工作。
把任务提取出来,目的就是为了解耦合
使用这种方法,适合多个线程
Thread t=new Thread(myRunnable); t.start(); Thread t1=new Thread(myRunnable); t1.start();
3、使用匿名内部类,实现创建Thread子类的方式
public class Demo3 {
public static void main(String[] args) {
Thread t=new Thread(){
@Override
public void run() {
while (true){
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
}
}
创建Thread的子类,同时实例化出一个对象
4、使用匿名内部类,实现实现Runnable接口的方式
public class Demo4 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
}
}
匿名内部类的实例,作为构造方法的参数
5、lambda表达式
public class Demo5 {
public static void main(String[] args) {
Thread t=new Thread(()->{
while (true){
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
多线程可以提高程序的效率?可以用代码来看一下
public class Demo6 {
public static final long COUNT=10000000000L;
public static void main(String[] args) {
//serial();
concurrency();
}
//串行执行任务
public static void serial(){
//记录ms级别的时间戳
long begin= System.currentTimeMillis();
long a=0;
for (long i = 0; i < COUNT; i++) {
a++;
}
a=0;
for (long i = 0; i < COUNT; i++) {
a++;
}
long end=System.currentTimeMillis();
System.out.println("执行的时间间隔:"+(end-begin)+"ms"); //14355ms
}
//并行执行任务
public static void concurrency(){
long begin=System.currentTimeMillis();
Thread t1=new Thread(()->{
long a=0;
for (long i = 0; i < COUNT; i++) {
a++;
}
});
Thread t2=new Thread(()->{
long a=0;
for (long i = 0; i < COUNT; i++) {
a++;
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end=System.currentTimeMillis();
System.out.println("执行的时间间隔:"+(end-begin)+"ms"); //5164ms
}
}
此处的两个join是两个线程都执行完,才继续执行计时操作。
Thread类
Thread常见构造方法
可以在创建线程的时候,给线程起名字
Thread(String name)
Thread(Runnable target,String name)
public static void main(String[] args) {
Thread t=new Thread(()->{
while (true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"这是我的线程");
t.start();
}
Thread常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
public class Demo8 {
public static void main(String[] args) {
Thread t=new Thread(()->{
while (true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"这是我的线程");
t.start();
System.out.println(t.getId());
System.out.println(t.getName());
System.out.println(t.getState());
System.out.println(t.getPriority());
System.out.println(t.isDaemon());
System.out.println(t.isAlive());
System.out.println(t.isInterrupted());
}
}
启动一个线程-start()
class MyThread2 extends Thread{
@Override
public void run() {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo9 {
public static void main(String[] args) {
MyThread2 t=new MyThread2();
t.start();
//t.run();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
直接调用run并没有创建线程,只是在原来的线程中运行的代码
调用start,则是创建了线程,在新线程中执行代码(和原来的线程是并发的)
线程的中断
1、直接定义一个标志位,作为线程是否结束的标志
public class Demo10 {
private static boolean isQuit=false;
public static void main(String[] args) {
Thread t=new Thread(()->{
while (!isQuit){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t线程执行完了");
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
isQuit=true;
System.out.println("设置让t线程结束");
}
}
2、使用标准库自带的标志位
Thread.currentThread().isInterrupted() 判定标志位
public static void main(String[] args) {
Thread t=new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//e.printStackTrace();
break;
}
}
System.out.println("t线程执行完了");
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
System.out.println("设置让t线程结束");
}
interrupt的方法行为有两种情况:
t线程在运行状态,会设置标志位为true;
t线程在阻塞状态,而是触发一个interruptedException,这个异常会提前把sleep唤醒,设置标志位但又给清除
等待一个线程-join()
线程之间的调度顺序是不确定的,join就是控制线程之间的结束顺序
public static void main(String[] args) {
Thread t=new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
System.out.println("main线程 join之前");
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main线程 join之后");
}
线程的状态
线程的所有状态
NEW:Thread对象创建出来了,但内核的PCB还没创建(还没有真正创建线程)
TERMINATED:内核的PCB销毁了,但是Thread对象还在
RUNNABLE:就绪状态(正在CPU运行+在就绪队列中排队)
TIMED_WAITING:按照一定的时间进行阻塞,sleep
WAITING:特殊的阻塞状态,调用wait
BLOCKED:等待锁的时候进入的阻塞状态
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
for (int i = 0; i < 100000000; i++) {
}
});
//t开始之前,得到的就是NEW
System.out.println(t.getState());
t.start();
//t正在工作,得到的是RUNNABLE
Thread.sleep(1);
System.out.println(t.getState());
t.join();
//t开始之后,得到的就是TERMINATED
System.out.println(t.getState());
}
NEW
RUNNABLE
TERMINATED
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
// for (int i = 0; i < 100000000; i++) {
//
// }
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//t开始之前,得到的就是NEW
System.out.println(t.getState());
t.start();
//t正在工作,得到的是RUNNABLE
Thread.sleep(50);
System.out.println(t.getState());
t.join();
//t开始之后,得到的就是TERMINATED
System.out.println(t.getState());
}
NEW
TIMED_WAITING
TERMINATED
线程安全
线程不安全
class Counter{
public int count=0;
public void increase(){
count++;
}
}
public class Demo14 {
private static Counter counter=new Counter();
public static void main(String[] args) throws InterruptedException {
//两个线程,每个线程都针对count进行5w的自增
//预期结果10w
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:"+counter.count); //71457
}
}
线程不安全的原因:
1、抢占式执行;
2、多个线程修改同一变量;
3、修改操作不是原子的(原子表示不可分割的最小单位)
4、内存可见性问题;
5、指令重排序
如何解决线程不安全问题
就上述代码而言,通过特殊手段,让count++变成原子的(加锁操作)
进行加锁,使用synchronized关键字
1、修饰方法
public synchronized void increase(){ count++; }
使用synchronized关键字来修饰一个普通方法,当进入方法时,就会加锁,方法执行完毕,自然解锁。
2、修饰代码块
把要进行加锁的逻辑放到synchronized修饰的代码块中,也能起到加锁的作用
3、修饰静态方法
锁对象相当于类对象(下边会介绍)
写法一
public void increase(){ //括号内是锁对象(被用来加锁的对象) synchronized (this){ count++; } }
写法二
public Object locker=new Object(); public void increase(){ synchronized (locker){ count++; } }
写法三
public static Locker locker=new Locker(); public void increase(){ synchronized (locker){ count++; } }
java中的任何一个对象,都可以作为锁对象(成员变量、局部变量、静态变量、类对象……),而且这些不同形态的对象,作为锁对象是没有任何区别。锁对象只是用来控制线程之间的互斥的。针对同一对象加锁,就会产生互斥,针对不同对象加锁,就不会产生互斥。
public void increase(){
synchronized (this){
count++;
}
}
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter2.increase();
}
});
此代码是针对不同对象进行加锁,不会出现锁竞争
public Object locker=new Object();
public void increase(){
synchronized (locker){
count++;
}
}
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();
}
});
此代码的对象是同一个,counter中的locker,则会出现锁竞争
public Object locker=new Object();
public void increase(){
synchronized (locker){
count++;
}
}
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter2.increase();
}
});
此代码的locker是两个对象,不涉及锁竞争
static private Object locker=new Object();
public void increase(){
synchronized (locker){
count++;
}
}
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter2.increase();
}
});
此时的locker是一个静态成员(类属性),类属性是唯一的(一个进程中,类对象只有一个,类属性也只有一份),因此,这两个locker实际上是一个locker,会产生锁竞争
static private Object locker=new Object();
public void increase(){
synchronized (locker){
count++;
}
}
public void increase2(){
synchronized (this){
count++;
}
}
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.increase2();
}
});
一个线程针对静态的locker对象加锁,一个则针对counter本身加锁,不同对象,则不会产生锁竞争
public void increase(){
synchronized (Counter.class){
count++;
}
}
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter2.increase2();
}
});
类对象,在JVM进程中只有一个,针对同一个Counter.class加锁,也会存在锁竞争
死锁
static class Counter{
public synchronized void increase(){
synchronized (this){
}
}
}
public static void main(String[] args) {
threading.Counter counter=new threading.Counter();
Thread t=new Thread(){
@Override
public void run() {
counter.increase();
}
};
t.start();
}
针对上述情况,不会产生死锁的锁,叫“可重入锁”
可重入锁的底层实现:t线程尝试针对this来加锁,this这个锁里边就记录了是t线程持有它,第二次进行加锁的时候,如果还是t线程,直接通过,不会阻塞等待(就是让锁记录好是哪个线程持有的它)
synchronized (this){
synchronized (this){
//
}
}
可重入锁的实现要点:
1、让锁里持有线程对象,记录是谁加了锁;
2、维护一个计数器,用来衡量撒时候是真加锁,撒时候是真解锁,撒时候是直接放行
线程不安全的原因-内存可见性问题
static class Counter{
public int count=0;
}
public static void main(String[] args) {
Counter counter=new Counter();
Thread t1=new Thread(()->{
while (counter.count==0){
}
System.out.println("t1执行结束");
});
t1.start();
Thread t2=new Thread(()->{
System.out.println("请输入一个int:");
Scanner scanner=new Scanner(System.in);
counter.count=scanner.nextInt();
});
t2.start();
}
t1读的是自己工作内存中的内容,当t2对变量count进行修改时,t1感知不到count的修改
这就是编译器优化在多线程环境下存在误判的问题
volatile关键字能保证内存可见性,提醒编译器不要优化。此事被修饰的变量,编译器就不会做出“不读内存,只读寄存器”这样的优化。
volatile不保证原子性,只能针对一个线程读,一个线程修改这个场景
static class Counter{
volatile public int count=0;
public void increase(){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
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);
}
而synchronized能保证原子性这个观点存疑。
wait和notify
wait
wait是Object的方法,是让当前线程进入等待状态
public static void main(String[] args) {
Object object=new Object();
System.out.println("wait之前");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait之后");
}
wait操作本质做了以下几件事:
1、释放当前锁;
2、进行等待通知;
3、满足一定条件时,被唤醒,然后尝试重新获取锁
线程执行到wait,就会发生阻塞,直到另一个线程,调用notify把这个wait唤醒
wait要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常
public static void main(String[] args) {
Object object=new Object();
System.out.println("wait之前");
synchronized (object){
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("wait之后");
}
在这里得保证加锁对象和调用wait对象是同一个,还要保证调用wait和notify的对象也是同一个
notify
notify方法是唤醒等待的线程
public static void main(String[] args) {
//准备一个对象,保证等待和调用是同一个
Object object=new Object();
//第一个线程
Thread t1=new Thread(()->{
while (true){
synchronized (object){
System.out.println("wait之前");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait之后");
}
}
});
t1.start();
//第二个线程,进行notify
Thread t2=new Thread(()->{
while (true){
synchronized (object){
System.out.println("notify之前");
object.notify();
System.out.println("notify之后");
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
}
notifyAll
多个线程都在wait,notify是随机唤醒一个;notifyAll是全部唤醒
多线程案例
单例模式
单个实例instance(对象)。某个类,有且只有一个实例。
单例模式本质上就是借助编程语言自身的语法特性,强行限制某个类,不能创建多个实例。
单例模式创建:
static修饰的成员/属性,变成类成员/类属性,当属性变为类属性时,此时就是单例模式了。更具体地说,是类对象的属性,而类对象是通过JVM加载.class文件来的。此时类对象,在JVM中也是“单例”,换句话说,JVM针对某个.class文件只会加载一次,也就只有一个类对象,类对象上面的成员(static修饰),也就只有一份。
//这个类,就作为一个”实例类“
//要求singleton只有一个实例
class Singleton{
private static Singleton instance=new Singleton();
public Singleton getInstance(){
return instance;
}
//把构造方法设为private,此时在类的外边,就无法继续new实例了
private Singleton(){
}
}
public class Demo20 {
public static void main(String[] args) {
Singleton instance=Singleton.getInstance();
}
}
使用这个唯一实例的方法就是通过getInstance方法,强制的保证了当前的Singleton是“单例”
上述代码中实现单例模式的方式叫做“饿汉模式”,是在类加载阶段创建的实例。
还有一种方式叫做“懒汉模式”
//懒汉模式来实现单例模式
class SingletonLazy{
private static SingletonLazy instance=new SingletonLazy();
public static SingletonLazy getInstance(){
if (instance==null){
instance=new SingletonLazy();
}
return instance;
}
}
public class Demo21 {
public static void main(String[] args) {
SingletonLazy instance=SingletonLazy.getInstance();
}
}
首次调用getInstance的时候才会触发创建实例的时机,后续调用getInstance,立即返回
由上述两个代码可知,懒汉模式是线程不安全的,getInstance方法既涉及了读,也涉及了修改。
class SingletonLazy{
private volatile static SingletonLazy instance=new SingletonLazy();
public static SingletonLazy getInstance(){
if (instance==null){
synchronized (SingletonLazy.class){
if (instance==null){
instance=new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){
}
}
public class Demo21 {
public static void main(String[] args) {
SingletonLazy instance=SingletonLazy.getInstance();
}
}
此时线程就是安全的,并且是在实例没有创建之前(线程不安全的时候)需要加锁的时候加,在实例创建之后(线程安全的时候)不加。
阻塞队列
1、线程安全的
2、带有阻塞功能
(a)如果队列满,继续入队列,入队列操作就会阻塞,直到队列不满,入队列才能完成
(b)如果队列空,继续出队列,出队列操作就会阻塞,直到队列不空,出队列才能完成
应用场景:生产者消费者模型,描述的是多线程协同工作的一种方式
阻塞队列的用处:
1、使用阻塞队列,有利于代码“解耦合”(耦合:两个模板之间的关联关系,关系越紧密,耦合越高,关系越不紧密,耦合越低)
2、削峰填谷
按照没有生产着消费者模型的写法,外面流量过来的压力,就会直接压到每个服务器上,如果某个服务器抗压能力不行,就容易挂。如果使用阻塞队列,当时当流量骤增的时候,队列承受了压力,其他的冲击力就不大
阻塞队列的具体使用
1、标准库的阻塞队列
public static void main(String[] args) throws InterruptedException {
BlockingDeque<Integer> blockingDeque=new LinkedBlockingDeque<>(100);
//带有阻塞功能的入队列
blockingDeque.put(1);
blockingDeque.put(2);
blockingDeque.put(3);
Integer ret=blockingDeque.take();
System.out.println(ret);
ret=blockingDeque.take();
System.out.println(ret);
ret=blockingDeque.take();
System.out.println(ret);
}
2、自己实现一个阻塞队列
先实现一个普通队列;加上线程安全;加上阻塞的实现
class MyBlockingQueue{
private int[] items=new int[1000];
private volatile int head=0;
private volatile int tail=0;
private volatile int size=0;
//入队列
public void put(int elem) throws InterruptedException {
synchronized (this){
//判定队列是否满了,满了则不能入队列
while (size>=items.length){
this.wait();
}
//进行插入操作,把elem放到items里,放到tail指向的位置
items[tail]=elem;
tail++;
if (tail>=items.length){
tail=0;
}
size++;
this.notify();
}
}
//出队列,返回删除元素的内容
public Integer take(){
synchronized (this){
//判定队列是否空了,空了则不能出队列
while (size==0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//进行取元素操作
int ret=items[head];
head++;
if (head>=items.length){
head=0;
}
size--;
this.notify();
return ret;
}
}
}
public class Demo23 {
public static void main(String[] args) {
MyBlockingQueue queue=new MyBlockingQueue();
Thread producer=new Thread(()->{
while (true){
try {
int n=1;
queue.put(1);
System.out.println("生产元素"+n);
n++;
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread customer=new Thread(()->{
while (true){
try {
int n=queue.take();
System.out.println("消费元素"+n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
customer.start();
}
}
定时器
1、标准库的定时器
public class Demo24 {
public static void main(String[] args) {
Timer timer=new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("时间到了,快起床");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("时间到了,快起床1");
}
},4000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("时间到了,快起床2");
}
},5000);
System.out.println("开始计时");
}
}
2、自己实现一个定时器
schedule第一个参数是一个任务,需要能够描述这个任务,任务包含两方面的信息,一个是要执行撒工作,一个是撒时候执行;
让MyTimer管理多个任务,使用BlockingQueue来实现;
任务已经被安排到优先级阻塞队列中,接下来需要从队列中取元素,创建一个单独的扫描线程,让这个线程不停的来检查队首元素,时间是否到了,如果时间到了,则执行该任务。
//这个类表示一个任务
class MyTask implements Comparable<MyTask>{
//要执行的任务
private Runnable runnable;
//什么时间来执行任务
private long time;
public MyTask(Runnable runnable,long delay){
this.runnable=runnable;
this.time=System.currentTimeMillis()+delay;
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {
return (int) (this.time-o.time);
}
}
class MyTimer{
private BlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
private Object locker=new Object();
public MyTimer(){
//创建一个扫描线程
Thread t=new Thread(()->{
while (true){
try {
synchronized (locker){
//取出队首元素
MyTask task=queue.take();
long curTime=System.currentTimeMillis();
if (curTime >= task.getTime()){
//到点了,该执行任务了
task.getRunnable().run();
}else {
//还没到点
queue.put(task);
locker.wait(task.getTime()-curTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
public void schedule(Runnable runnable,long after) throws InterruptedException {
MyTask myTask=new MyTask(runnable,after);
queue.put(myTask);
synchronized (locker){
locker.notify();
}
}
}
public class Demo25 {
public static void main(String[] args) throws InterruptedException {
MyTimer timer=new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("时间到!");
}
},3000);
System.out.println("开始计时");
}
}
线程池
线程诞生的目的就是进程太重量了,导致进程的创建和销毁比较低效。
从线程池取线程,把线程放回线程池,这是纯用户态实现的逻辑;从系统这里创建线程,则是用户态->内核态共同完成的逻辑。
这里可以使用线程池来进一步优化线程的速度,创建一个池子,创建很多线程,当需要执行任务的时候,不需要重新创建线程了,而是直接从池子里取一个现成的线程,直接使用,用完了,也不释放线程,而是还回到线程池里。
1、标准库实现线程池
此处创建线程池,是通过Executors类的静态方法newCachedThreadPool来完成-工厂模式
public class Demo26 {
public static void main(String[] args) {
ExecutorService pool= Executors.newCachedThreadPool();
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("这是任务");
}
});
}
}
2、自己实现线程池
class MyThreadPool{
private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
public MyThreadPool(int m){
//在构造方法中,创建出M个线程,负责完成工作
for (int i = 0; i < m; i++) {
Thread t=new Thread(()->{
while (true){
try {
Runnable runnable=queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
}
public class Demo27 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool=new MyThreadPool(10);
for (int i = 0; i < 1000; i++) {
int taskId=i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("执行当前任务:"+taskId+" 执行线程"+Thread.currentThread().getName());
}
});
}
}
}
常见的锁策略
乐观锁和悲观锁
乐观锁:预测锁冲突的概率不高,因此做的工作可以简单一点
悲观锁:预测锁冲突的概率较高,因此所做的工作就要复杂一点
读写锁和普通互斥锁
普通的互斥锁,就如同synchronized,当两个线程竞争同一把锁,就会产生等待
读写锁分为加读锁和加写锁
读锁和读锁之间,不会产生竞争;写锁和写锁之间,有竞争;读锁和写锁之间,有竞争
重量级锁和轻量级锁
重量级锁,加锁解锁开销比较大,典型的进入内核态的加锁逻辑
轻量级锁,加锁解锁开销比较小,典型的纯用户态的加锁逻辑
自旋锁和挂起等待锁
自旋锁,是一种轻量级锁的典型实现,自旋就类似于“忙等”,消耗大量的CPU,反复询问当前锁是否就绪
挂起等待锁,是重量级锁的一种典型实现
公平锁和非公平锁
公平锁:遵守“先来后到”
不公平锁:不遵守“先来后到”
操作系统内部的线程调度可以认为是随机的,如果不做任何额外的限制,锁就是非公平锁。如果要想实现公平锁,就需要依赖额外的数据结构,来记录线程们的先后顺序
可重入锁和不可重入锁
可重入锁:同一线程针对同一把锁,连续加锁两次,不会死锁
不可重入锁:同一线程针对同一把锁,连续加锁两次,会死锁
CAS
compare and swap
把内存中某个值,和CPU寄存器A中的值,进行比较。如果两个值相同,就把另一个寄存器B中的值和内存的值进行交换(把内存的值放到寄存器B,同时把寄存器B的值写给内存)
boolean CAS(address,expectValue,swapValue){
if (&address==expectedValue){
&address=swapValue;
return true;
}
return false;
}
上述代码,是SS通过一个CPU指令完成的,是原子的
基于CAS实现一些逻辑的时候,不加锁,也能保证线程安全,但只能在特定场景使用。加锁适用面更广,加锁代码往往比CAS可读性更好
最常用的两个场景:
1、实现原子类
2、实现自旋锁
CAS的ABA问题
在CAS中,进行比较的时候,发现寄存器A和内存M的值相同,但无法判断M始终没变,还是M变了,又变回来了
如何解决ABA问题?
只要有一个记录,能够记录上内存中数据的变化,也就是保存M的修改次数或者是上次修改时间,这两个都是只增不减的
Synchronized原理
Synchronized具有以下特性:1、开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁;2、开始是轻量级锁实现,如果锁被持有的时间较长,就转换为重量级锁;3、实现轻量级锁的时候大概率用到的自旋锁策略;4、是一种不公平锁;5、是一种可重入锁;6、不是读写锁
锁升级/锁膨胀
synchronized效果是“加锁”,当两个线程针对同一个对象加锁的时候,就会出现锁竞争,后来尝试加锁的线程就得阻塞等待,直到前一个线程释放锁
synchronized加锁的具体过程:1、偏向锁;2、轻量级锁‘3、重量级锁
无竞争,偏向锁;有竞争,轻量级锁;竞争激烈,重量级锁
锁消除
JVM自动判定,如果发现这个地方的代码,不必加锁,如果你写了synchronized,就会自动的把锁干掉
锁粗化
synchronized对应代码块包含多少代码,包含的代码少,粒度细,包含的代码粗,粒度粗
锁粗化,就是把细粒度的加锁=》粗粒度的加锁
JUC
Callable接口
类似于Runnable,Runnable描述的任务,不带返回值;Callable描述的任务,带返回值
//创建线程,计算1+2+……+1000
public static void main(String[] args) throws ExecutionException, InterruptedException {
//使用Callable定义一个任务
Callable<Integer> callable=new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum=0;
for (int i = 0; i < 1000; i++) {
sum+=i;
}
return sum;
}
};
FutureTask<Integer> futureTask=new FutureTask<>(callable);
//创建线程,执行任务
//Thread的构造方法,不能直接传Callable,还需要一个中间的类
Thread t=new Thread(futureTask);
t.start();
//获取线程的计算结果
//get方法会阻塞,直到call方法计算完毕,get才会返回
System.out.println(futureTask.get());
}
ReentrantLock
也是可重入锁,ReentrantLock是对synchronized的一个补充
核心用法:1、lock()加锁;2、unlock()解锁
public static void main(String[] args) {
ReentrantLock locker=new ReentrantLock(true);
try {
locker.lock();
}finally {
locker.unlock();
}
}
优点:
1、tryLock试试看能不能加上锁,试成功了,就加锁成功,试失败了,就放弃,并且还可以指定加锁的等待超时时间;
2、ReentrantLock可以实现公平锁,默认是非公平的,构造的时候,传入一个参数,就成了公平锁
ReentrantLock locker=new ReentrantLock(true);
3、synchronized是搭配wait/notify实现等待通知机制的,唤醒操作是随机唤醒一个等待的线程
ReentrantLock搭配Condition类实现的,唤醒操作是可以指定唤醒哪的等待的线程的
原子类
在没有加锁的情况下,实现多线程的累加
public static void main(String[] args) throws InterruptedException {
AtomicInteger count=new AtomicInteger(0);
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
//相当于count++
count.getAndIncrement();
// //相当于++count
// count.incrementAndGet();
// //相当于count--
// count.getAndDecrement();
// //相当于--count
// count.decrementAndGet();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
//相当于count++
count.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count.get());
}
线程池
信号量Semaphore
信号量的基本操作:p操作,申请一个资源;v操作,释放一个资源
信号量本身是一个计数器,表示可用资源的个数,p操作申请一个资源,可用资源数就-1;v操作释放一个资源,可用资源数就+1,当计数器为0的时候,继续p操作,就会产生阻塞,阻塞等待到其它线程v操作了为止
信号量可以被视为一个更广义的锁
CountDownLatch
同时等待N个任务执行结束
public static void main(String[] args) throws InterruptedException {
//有10个选手参加了比赛
CountDownLatch countDownLatch=new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
Thread t=new Thread(()->{
System.out.println("选手出发"+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("选手到达"+Thread.currentThread().getName());
//撞线
countDownLatch.countDown();
});
t.start();
}
//await是进行阻塞等待,会等到所有的选手都撞线之后,才能解除阻塞
countDownLatch.await();
System.out.println("比赛结束");
}
线程安全的集合类-CopyOnWrite
“双缓冲区”机制,写(修改)时拷贝(复制)
如果要修改,不直接修改,而是把原来的数据复制一份,修改的时候改这个新的数据
多线程环境下使用哈希表
HashTable
不推荐使用
ConcurrentHashMap
优化策略:
1、锁粒度的控制
HashTable直接在方法上加synchronized,相当于是对this加锁。相当于是针对哈希表对象加锁,一个哈希表只有一把锁。多个线程,无论这些线程,都是如何操作的这个哈希表,都会产生所冲突。
ConcurrentHashMap每个哈希桶有自己的锁,大大降低了锁冲突的概率,性能也就大大提高了。
2、ConcurrentHashMap做了一个激进的操作
只是给写操作加锁,读操作不加锁。两个线程同时修改,才会有锁冲突;两个线程同时读,不会有锁冲突;如果一个线程读,一个线程修改,也没有锁冲突。
3、充分的利用到了CAS的特性
像维护元素个数,都是通过CAS来实现的,而不是加锁,还有的地方直接使用CAS实现的轻量级锁来实现
ConcurrentHashMap思路就是能不加锁就不加
4、ConcurrentHashMap对于扩容操作,进行了特殊的优化,化整为零
HashTable的扩容机制:当put元素的时候,发现当前的负载因子已经超过阈值,就需要触发扩容,申请一个更大的数组,然后把之前旧的数据给搬运到新的数组上
ConcurrentHashMap在扩容的时候,不是一次性全部搬完,而是搬运一点。扩容的时候,旧的和新的会同时存在一段时间,每次进行哈希表的操作,都会把旧的内存上的元素搬运一部分到新的空间上,直到最终搬运完成,此时在释放旧的空间
死锁
场景1:一个线程,一把锁,线程连续加锁两次,如果这个锁是不可重入锁,发生死锁
场景2:两个线程,两把锁
public static void main(String[] args) throws InterruptedException {
Object locker1=new Object();
Object locker2=new Object();
Thread t1=new Thread(()->{
System.out.println("t1尝试获取locker1");
synchronized (locker1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1尝试获取locker2");
synchronized (locker2){
System.out.println("t1获取两把锁成功");
}
}
});
Thread t2=new Thread(()->{
System.out.println("t2尝试获取locker2");
synchronized (locker2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2尝试获取locker1");
synchronized (locker1){
System.out.println("t1获取两把锁成功");
}
}
});
t1.start();
t2.start();
}
场景3:多个线程多把锁
解决死锁
约定,加多个锁的时候,必须先加编号小的锁,后加编号大的锁,有效避免循环等待
public static void main(String[] args) throws InterruptedException {
Object locker1=new Object();
Object locker2=new Object();
Thread t1=new Thread(()->{
System.out.println("t1尝试获取locker1");
synchronized (locker1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1尝试获取locker2");
synchronized (locker2){
System.out.println("t1获取两把锁成功");
}
}
});
Thread t2=new Thread(()->{
System.out.println("t2尝试获取locker1");
synchronized (locker1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2尝试获取locker2");
synchronized (locker2){
System.out.println("t1获取两把锁成功");
}
}
});
t1.start();
t2.start();
}
多线程的知识就到此为止了,大家也可以去搜些资料多扩充一下!