【java基础】进程与线程 并发与并行 多线程 线程安全 锁 线程的生命周期 死锁活锁 线程池

1.进程与线程

1.进程:程序执行时的实例,也就是运行中的程序.
程序加载在内存上,且正在运行,才叫进程
每个进程都有对立的内存地址空间

进程里的堆,是一个进程中最大的一块内存,被进程中的所有线程共享的,进程创建时分配,主要存放 new 创建的对象实例
进程里的方法区,是用来存放进程中的代码片段的,是线程共享的

2.线程:线程是进程的执行单元,每一个线程可以执行一个任务
(线程就是给程序抢夺CPU时间片,抢到时间程序运行,没抢到程序等待)

为什么要有线程

不可能执行程序的一个功能就创建一个进程(以杀毒软件为例,不可能清理垃圾打开一个软件界面,然后同时杀毒的时候又打开一个软件界面),所以就有了线程
3.线程:CPU 调度和分派的基本单位,进程中的一个实体,线程本身是不会独立存在
1)系统不会为线程分配内存,线程组之间只能共享所属进程的资源

线程只拥有在运行中必不可少的资源(如程序计数器、栈)
线程里的程序计数器就是为了记录该线程让出 CPU 时候的执行地址,待再次分配到时间片时候就可以从自己私有的计数器指定地址继续执行
每个线程有自己的栈资源,用于存储该线程的局部变量和调用栈帧,其它线程无权访问

2)Java编写程序时

java 中当我们启动 main 函数时候就启动了一个 JVM 的进程,而 main
函数所在线程就是这个进程中的一个线程,也叫做主线程
一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器,栈区域
在这里插入图片描述

区别

内存分配:系统在运行的时候会为每个进程分配不同的内存空间,建立数据表来维护代码段、堆栈段和数据段;除了 CPU 外,系统不会为线程分配内存,线程所使用的资源来自其所属进程的资源
资源拥有:进程之间的资源是独立的,无法共享;同一进程的所有线程共享本进程的资源,如内存,CPU,IO 等
开销:每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行程序计数器和栈,线程之间切换的开销小
通信:进程间 以IPC(管道,信号量,共享内存,消息队列,文件,套接字等)方式通信;同一个进程下,线程间可以共享全局变量、静态变量等数据进行通信,做到同步和互斥,以保证数据的一致性
调度和切换:线程上下文切换比进程上下文切换快,代价小
执行过程:每个进程都有一个程序执行的入口,顺序执行序列;线程不能够独立执行,必须依存在应用程序中,由程序的多线程控制机制控制
健壮性:每个进程之间的资源是独立的,当一个进程崩溃时,不会影响其他进程;同一进程的线程共享此线程的资源,当一个线程发生崩溃时,此进程也会发生崩溃,稳定性差,容易出现共享与资源竞争产生的各种问题,如死锁等
可维护性:线程的可维护性,代码也较难调试,bug 难排查
ava 编程语言中线程是通过 java.lang.Thread 类实现的。

5.Java 编程语言中线程是通过 java.lang.Thread 类实现的,。
Thread 类中包含 tid(线程id)、name(线程名称)、group(线程组)、daemon(是否守护线程)、priority(优先级) 等重要属性。

并发与并行

1.并发(多线程在同一个CPU上):某个一个时刻只有一个事件,在某个时间段多个事件交替执行,并发操作系统会根据任务调度系统给线程分配线程的 CPU 执行时间,线程的执行会进行切换。并发对CPU资源进行抢占
并发编程三大问题

CPU 缓存,在多核 CPU 的情况下,带来了可见性问题
操作系统对当前执行线程的切换,带来了原子性问题
编译器指令重排优化,带来了有序性问题

2.并行(多线程在多个CPU上):多个事件在同一个时刻发生

线程的调度方式

抢占式调度 :线程去抢夺CPU时间,谁抢到谁就执行,并且谁抢到都是随机的,我们无法干预(线程抢到时间给程序,有时间程序就运行,没时间程序就等待)

2.多线程

多线程程序的实现

Thread也实现了Runable接口
Runable接口中只有一个run方法

1.Thread的第一种实现方式

以子类的方式 继承Thread类(因为继承只能继承一个父类不是太方便,不常用)
继承Thread类,重写run方法,方法中的代码需要CPU时间片,使用start()方法来启动线程,抢夺CPC时间,抢到后执行run()方法

步骤

1.创建一个类,继承Thread
2.重写Thread类中的方法
3.在测试类中创建Thread的子类对象
4.调用start启动线程

特别提醒
当通过一个线程调用start方法后,会做两件事情
1.这个线程会启动,去抢夺CPU时间
2.抢到CPU时间之后这个线程会自动执行自己的run方法
如果直接调用run()方法,就没有启动线程去抢夺CPU时间,不属于线程

package pack01;

class Thread01  extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("循环1:"+i);
        }
    }
}

class Thread02  extends Thread{
    @Override
    public void run() {
        for (int j = 0; j < 10; j++) {
            System.out.println("循环2:"+j);
        }
    }
}

public class Demo01 {
    public static void main(String[] args) {
      Thread01 t1=new Thread01();
        Thread02 t2=new Thread02();
        t1.start();
        t2.start();
    }
}

main主线程

当程序启动时,JVM会创建一个main线程,并执行程序中的main方法
JVM先找到main,将main方法内的代码放到mainThread去执行

Thread类中有许多的静态方法,不需要创建对象,只有Thread类就能直接调用

获得线程名称,修改线程名

  1. super调用setName,getName

super.getName(); 获得线程名称
super.getName(“线程一”);修改线程名

2.构造方法 super()调用父类的构造方法

class Thread1 extends Thread{
    public Thread1(String name) {
        super(name);//super()调用父类的构造方法
    }

    @Override
    public void run() {
        //2: 重写run方法,方法内的代码需要cpu时间执行
        //调用setName修改线程的名称
        //super.setName("线程1号");
        System.out.println(super.getName());//获取线程的名称
        //存放代码
        for (int i = 0; i < 100; i++) {
            System.out.println("循环1 i =  "+i);
        }

    }
}

拿到当前的线程

在Thread类中static Thread currentThread() 为静态方法,所以可以通过类直接调用

Thread t=Thread.currentThread();

休眠 static Thread sleep(毫秒)

1000毫秒=1秒
在休眠时,线程不去抢占CPU时间,休眠结束后再去抢夺
每两秒打印一次时间

package pack01;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Demo02 {
    public static void main(String[] args) {
       SimpleDateFormat format= new SimpleDateFormat("yyyy-mm-dd hh:mm:ss");
        try {
            while (true){
                Thread.sleep(2*1000);
                System.out.println(format.format(new Date()));
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

2.Thread的第二种实现方式 实现Runnable接口Runnable相等于一个任务,让线程去抢夺CPU时间,抢到了才可以执行Runnable

实现类的方式:实现Runnable 再把实现类对象给Thread对象(常用,方便数据共享,线程池(就是为了线程池而特意存在的))

JVM只有看到Thread才知道是线程,所以Task要调用Thread构造方法 Thread(Runnable r)

实现步骤

1.定义类,然后实现Runnable接口
2.重写这个接口中的run方法,在run方法中定义线程要执行的任务
3.在测试类中创建Runnable实现类对象
4.创建Thread对象,并且在构造方法位置传递Runnable实现类对象
5.调用线程的start方法,开启线程,线程会执行对应的run方法

//1 实现类的方式:Runnable
class Task1 implements Runnable{
    @Override
    public void run() {
        //2:执行代码
        for (int i = 0; i < 100; i++) {
            System.out.println("循环1 i =  "+i);
        }

    }
}
class Task2 implements Runnable{
    @Override
    public void run() {

        for (int j = 0; j < 100; j++) {
            System.out.println("循环2 j =  "+j);
        }
    }
}
public class Demo04 {
    public static void main(String[] args) {

        //JVM只看Thread
        Task1 task1 = new Task1();
        Task2 task2 = new Task2();
        //为后续线程池做准备
        Thread t1 = new Thread(task1);//调用Thread构造方法 Thread(Runnable r)
        Thread t2 = new Thread(task2);
        t1.start();
        t2.start();
    }
}

在这里插入图片描述

第二种实现方式的好处

1.实现接口的方式解决了java类中类与类之间 单继承的局限性
2.Runnable 接口中只有一个 run方法,没有start,setName,getName,这些方法,在Runnable接口中只需要关注线程要执行的任务,这样的话,功能更加的纯粹,更加符合设计模式中的单一职责原则
3.降低耦合性
4.第二种方式可以更加简便的实现多个线程之间的数据共享,线程池

3.线程安全

为什么会有线程安全,

多个线程去修改同一个共享数据,就会有线程安全问题,可以用同步关键字或者锁来解决这一问题

Lock锁

在jdk1.5之后,提供了Lock接口,这个Lock接口表示的是锁,支持手动的获取锁以及手动的释放锁

Lock是一个接口,如果要用需要使用它的实现类我们可以使用实现类 ReentrantLock

步骤

1.创建锁对象 (可以创建任何地方,且唯一)

Private static final Lock lock=new ReentratLock();  

2.在写数据的地方处加锁,写完就释放锁.保证一次就一个线程修改数据
void lock():手动的获取锁
void unlock():手动的释放锁

class Task  implements Runnable{
    int ticket=100;
    //创建锁对象
    Lock lock= new ReentrantLock();
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
      while (true){
          //加锁
          lock.lock();
          if(ticket>=0){
              ticket--;
              System.out.println("还剩:"+ticket+"张");
          }else {
              break;
          }
          //释放锁
          lock.unlock();
  }
    }
}

synchronize

同步代码块

只有持有锁对象(lock=true)的线程才能够进行到同步代码块。

锁对象:只是一个标记,相当于true,false,任何对象都可以.如果多个线程共用一个对象,该对象可以为锁对象

 Object lock =new Object();

同步代码块的作用
保证只有持有锁对象的线程才能进入到同步代码块中, 这样就可以避免多个线程共同操作的共享数据

例子

class SaleTicket implements Runnable {
    int tikets = 100;//1: 100张票

    //锁对象:只是一个标记相当于true,false,任何对象都可以
    private  static  final Object lock = new Object();
    @Override
    public void run() {
        //逻辑代码
        while (true) {
            try {
                Thread.sleep(50);//磨叽
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //判断是否有锁,如果有锁则获得锁,进入代码块,否则,只有在代码块外等锁
           synchronized (lock){//获得锁
               //如果票数> 0 ,退出
               if (tikets > 0) {
                   tikets--;
                   System.out.println("还剩余" + tikets + "张");
               }else{
                   break;
               }
           }//释放锁

        }

    }
}
同步方法(是同步代码块的变形)

同步方法相当于把同步代码块拿出run方法进行封装,然后到run方法中调用同步方法
同步方法的锁对象:
同步方法是普通方法,则为 this 对象
同步方法是静态方法,则当前类类名.class

class SaleTicket implements Runnable {
    static int tikets = 100;//1: 100张票

    //锁对象:只是一个标记相当于true,false,任何对象都可以

    @Override
    public void run() {
        //逻辑代码
        while (true) {
            try {
                Thread.sleep(50);//磨叽
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            sell2();

        }

    }
    //判断是否有锁,如果有锁则获得锁,进入代码块,否则,只有在代码块外等锁
    synchronized void sell (/* this */){//获得锁
        //如果票数> 0 ,退出
        if (tikets > 0) {
            tikets--;
            System.out.println("还剩余" + tikets + "张");
        }
    }//释放锁

    //判断是否有锁,如果有锁则获得锁,进入代码块,否则,只有在代码块外等锁
    synchronized static void sell2 (/* SaleTicket.class */){//获得锁
        //如果票数> 0 ,退出
        if (tikets > 0) {
            tikets--;
            System.out.println("还剩余" + tikets + "张");
        }
    }//释放锁
}
注意事项

synchronized 修饰代码块时,最好不要锁定基本类型的包装类,如 jvm 会缓存 -128 ~ 127 Integer 对象,每次向如下方式定义 Integer 对象,会获得同一个 Integer,如果不同地方锁定,可能会导致诡异的性能问题或者死锁
Integer i = 100;
synchronized 修饰代码块时,要线程互斥地执行代码块,需要确保锁定的是同一个对象,这点往往在实际编程中会被忽视
synchronized 不支持尝试获取锁、锁超时和公平锁

死锁

(1)两个以上的线程,每个线程各自持有一把锁,并且等待其他线程释放锁。
(2)死锁不是好的代码,是有问题的代码
在这里插入图片描述

线程一要想打开门A,必须要同时有lock1,lock2,线程二也是 现在 线程一拿到了lock1 打开了门A的第一把锁,线程二拿到了lock2
打开了门B的第一把锁,但是线程一不把lock1给线程二,线程二无法打开第二把锁,线程也不把lock2给线程一,线程一也无法打开第二把锁,所有就一直僵持,造成死锁

package pack02;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;



public class Demo04 {
    public static Object lock1=new Object();
    public static Object lock2=new Object();
    public static class Task1 implements  Runnable{

        @Override
        public void run() {
            synchronized (lock1){
                System.out.println("拿到第一道门的lock1锁");
                synchronized (lock2){
                    System.out.println("拿到第一道门的lock2锁,打开门");
                }
            }
        }
    }

    public static class Task2 implements  Runnable{

        @Override
        public void run() {
            synchronized (lock2){
                System.out.println("拿到第二道门的lock2锁");
                synchronized (lock1){
                    System.out.println("拿到第二道门的lock1锁,打开门");
                }
            }
        }
    }

    public static void main(String[] args) {
        Task1 task1=new Task1();
        Task2 task2=new Task2();
        Thread t1 = new Thread(task1);
        Thread t2 = new Thread(task2);
        t1.start();
        t2.start();
    }
}

4.线程的生命周期

在这里插入图片描述

在这里插入图片描述

(1)新建 new Thread() new Thread子类()
(2)运行(调用start方法,线程去抢夺CPU时间,抢到了就运行run方法)
(3)受阻塞
当一个线程没有拿到锁,只能等待其他线程释放锁
当一个线程等到了锁,就可以回到运行状态
(4)计时等待
当一个线程调用了sleep(millis)或者wait(millis) 睡醒之后,不需要锁,改成运行。 需要锁,改成 受阻塞。
(5)无限等待
当一个线程调用了wait方法(这个是Object的方法)那么这个线程就会进入无限等待,除非调用notify
改成 运行。 不需要锁
改成 受阻塞。 需要锁
(6)退出
当线程的run方法结束或者调用了 stop方法

5.线程池

1.我们之前使用线程,都是用到的时候直接创建线程,使用完了之后这个线程就变成退出状态,以后就再也不能用了。这样步骤的创建以及销毁线程会极大的影响效率
2.线程池是什么?
线程池就是一种容器,这个容器中的线程具有复用的特点。
当有任务要执行的时候,直接使用线程池中的线程去执行。
当任务执行完成,这个线程就会重新还回到线程池,这个线程还可以去执行其他任务
3.使用步骤
1.通过Executors工具类获取一个线程池的对象
2.定义Runnable接口实现类,里面定义线程池要执行的任务
3.调用线程池的submit方法,传递Runnable接口的实现类,表示要执行的任务
4.销毁线程池(一般不去做。)

 Executor接口
            |--ExecutorService子接口
            |--AbstractExecutorService子接口
            |--ThreadPoolExecutor实现类
            所以Executors只是一个快速创建线程池的工具,封装了ThreadPool1Executor实现类
         */
        ExecutorService service= Executors.newFixedThreadPool(2);//返回一个线程池对象
        service.submit(task1);//提交  本质是从线程池是获取一个线程
        //成功的话,就执行任务
        //等待 成功的话,就执行任务
        service.submit(task2);

6.线程的协作 线程的等待与唤醒 wait() notify() 方法

多个线程可以竞争,也可以协作
void wait():让当前线程等待,如果没有人唤醒他,就一直等
void wait(long timeout):让当前线程等待,如果到了指定的毫秒值还没有人唤醒他就自己醒
void notify() 唤醒一个线程,唤醒的是当前对象锁下的一个线程
void notifyAll() 唤醒所有线程,唤醒的是当前对象锁下面的所有线程

注意

1.这些方法一定要放在同步代码块中去使用,并且这些方法要通过锁对象去调用
2.wait()方法和notify方法()是Object类的方法,所以所以的对象都能调用
3.等待和唤醒必须是同一个对象,不同对象是无法唤醒另一个对象的wait()方法的

包子铺模型(一次生产一个,消费一个)

1:逻辑要正确
3.多线程共用一个对象时,该对象可以作为锁对象,该模型的锁对象为Cangku
2: notify wait写在同步代码块中,使用同一个对象 仓库
在这里插入图片描述


package pack03;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.SynchronousQueue;

class Cangku{
    public int num=0;
}

class Provider implements Runnable{
    private Cangku cangku;

    public Provider(Cangku cangku) {
        this.cangku = cangku;
    }

    @Override
    public void run() {
        while (true){
            synchronized(this.cangku){
                if(this.cangku.num!=0){
                    try {
                        System.out.println("还有包子,生产者等待中");
                        this.cangku.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } //end if
                else{
                    this.cangku.num++;  //生产包子
                    System.out.println("生产者生产包子:"+this.cangku.num);
                    //唤醒消费者来吃包子
                    this.cangku.notify();
                }

            } //end synchronized
        } //end while
    }   //end run
}

class Consumer implements Runnable{
    private Cangku cangku;

    public Consumer(Cangku cangku) {
        this.cangku = cangku;
    }

    @Override
    public void run() {
        while (true){
            synchronized (this.cangku){
                if(this.cangku.num==0){
                    try {
                        System.out.println("没有包子,消费者等待中");
                        this.cangku.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }//end if
                else {
                    //有包子可以吃
                    this.cangku.num--;  //吃了一个
                    System.out.println("消费者消费包子:"+this.cangku.num);  //吃完就没有包子
                    //唤醒生产者生产包子
                    this.cangku.notify();
                }

            }//end synchronized
        }//end while
    }
}

public class Demo {
    public static void main(String[] args) {
        Cangku cangku=new Cangku();
        //任务对象
         Provider provider=new Provider(cangku);
         Consumer consumer=new Consumer(cangku);
// 定义线程池
       ExecutorService pool=Executors.newFixedThreadPool(2);
       //获取线程
       pool.submit(provider);
       pool.submit(consumer);
    }
}

1.为什么要锁
在并发编程中,经常会遇到多个线程访问同一个共享变量,当同时对共享变量进行读写操作时,就会产生数据不一致的情况。
2.有哪些锁
JDK 1.5 之前,使用 synchronized 关键字,拿到 Java 对象的锁,保护锁定的代码块。JVM 保证同一时刻只有一个线程可以拿到这个 Java 对象的锁,执行对应的代码块。
JDK 1.5 开始,引入了并发工具包 java.util.concurrent.locks.Lock,让锁的功能更加丰富。

1)可重入锁
指在同一个线程在外层方法获取锁的时候,进入内层方法会自动获取锁。JDK 中基本都是可重入锁,避免死锁的发生。上面提到的常见的锁都是可重入锁。
2)公平锁 / 非公平锁
公平锁,指多个线程按照申请锁的顺序来获取锁。如 java.util.concurrent.lock.ReentrantLock.FairSync
非公平锁,指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程先获得锁。如 synchronized、java.util.concurrent.lock.ReentrantLock.NonfairSync
3)独享锁 / 共享锁
独享锁,指锁一次只能被一个线程所持有。synchronized、java.util.concurrent.locks.ReentrantLock 都是独享锁
共享锁,指锁可被多个线程所持有。ReadWriteLock 返回的 ReadLock 就是共享锁
4)悲观锁 / 乐观锁
悲观锁,一律会对代码块进行加锁,如 synchronized、java.util.concurrent.locks.ReentrantLock
乐观锁,默认不会进行并发修改,通常采用 CAS 算法不断尝试更新
悲观锁适合写操作较多的场景,乐观锁适合读操作较多的场景
5)粗粒度锁 / 细粒度锁
粗粒度锁,就是把执行的代码块都锁定
细粒度锁,就是锁住尽可能小的代码块,java.util.concurrent.ConcurrentHashMap 中的分段锁就是一种细粒度锁
粗粒度锁和细粒度锁是相对的,没有什么标准
6)偏向锁 / 轻量级锁 / 重量级锁
JDK 1.5 之后新增锁的升级机制,提升性能。
通过 synchronized 加锁后,一段同步代码一直被同一个线程所访问,那么该线程获取的就是偏向锁
偏向锁被一个其他线程访问时,Java 对象的偏向锁就会升级为轻量级锁
再有其他线程会以自旋的形式尝试获取锁,不会阻塞,自旋一定次数仍然未获取到锁,就会膨胀为重量级锁
7)自旋锁
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环占有、浪费 CPU 资源

锁池(多个线程想要拿到对象A同步方法,但是每次只有一个能拿到,所以其他线程就在锁池中等待)

当前线程想调用对象A的同步方法时,发现对象A的锁被别的线程占有,此时当前线程进入锁池状态。简言之,锁池里面放的都是想争夺对象锁的线程。
当一个线程1被另外一个线程2唤醒时,1线程进入锁池状态,去争夺对象锁。
锁池是在同步的环境下才有的概念,一个对象对应一个锁池。

等待池

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池,等待池中的线程不会去竞争该对象的锁。

volatile关键字

表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序
1.作用
保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了共享变量的值,共享变量修改后的值对其他线程立即可见
通过禁止编译器、CPU 指令重排序和部分 happens-before 规则,解决有序性问题

synchronized和volatile的区别是什么?

synchronized 可以作用于变量、方法、对象;volatile 只能作用于变量。
synchronized 可以保证线程间的有序性(个人猜测是无法保证线程内的有序性,即线程内的代码可能被 CPU 指令重排序)、原子性和可见性;volatile 只保证了可见性和有序性,无法保证原子性。
synchronized 线程阻塞,volatile 线程不阻塞。
volatile 本质是告诉 jvm 当前变量在寄存器中的值是不安全的需要从内存中读取;sychronized 则是锁定当前变量,只有当前线程可以访问到该变量其他线程被阻塞。
volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

lock接口

注意事项
ReentrantLock 一定要记得在 finally{} 语句块中调用 unlock() 方法释放锁,不然可能导致死锁
ReentrantLock 在并发量很高的情况,由于自旋很消耗 CPU 资源
ReentrantReadWriteLock 适合对共享资源写操作很少,读操作频繁的场景;可以从写锁降级到读锁,无法从读锁升级到写锁

synchronized 和 java.util.concurrent.lock.Lock 之间的区别

synchronized:非常适合写(改变共享变量)多、读少的场景
java.util.concurrent.lock.Lock:非常适合读多、写少的场景

实现层面不一样。synchronized 是 Java 关键字,JVM层面 实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁
是否自动释放锁。synchronized 在线程代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要在 finally {} 代码块显式地中释放锁
是否一直等待。synchronized 会导致线程拿不到锁一直等待;Lock 可以设置尝试获取锁或者获取锁失败一定时间超时
获取锁成功是否可知。synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 获得加锁是否成功
功能复杂性。synchronized 加锁可重入、不可中断、非公平;Lock 可重入、可判断、可公平和不公平、细分读写锁提高效率

死锁与活锁的区别,死锁与饥饿的区别

死锁

是指两个或者两个以上的进程(或线程)在执行过程中, 因争夺共享资源而造成的一种互相等待的现象, 若无外力作用,他们将无法推进下去。
1.产生死锁的共享条件

互斥条件:共享资源被一个线程占用 ,其他线程一直等待
请求与保持条件(占有且等待):线程 t1 已经取得共享资源 s1,尝试获取共享资源 s2 的时候,不释放共享资源 s1
不可剥夺条件(不可抢占):其他线程不能强行抢占线程 t1 占有的资源 s1
循环等待条件:线程 t1 等待线程 t2 占有的资源,线程 t2 等待线程 t1 占有的资源

只需要破坏上面 4 个条件中的一个就能破坏。

互斥是多线程的特性,因为加锁就是为了保证互斥,所以这个条件无法避免,
请求与保持条件:一次性申请所有的资源,破坏 “占有且等待” 条件
不剥夺: 占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源,破坏 “不可抢占” 条件, Lock 的 tryLock() 方法,获取锁失败释放所有资源
循环等待:按序申请资源,破坏 “循环等待” 条件

活锁

任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试、失败、尝试、失败。在这期间线程状态会不停的改变
饥饿

饥饿

1.一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。一直有线程级别高的暂用资源,线程低的一直处在饥饿状态。 比如ReentrantLock显示锁里提供的不公平锁机制,不公平锁能够提高吞吐量但不可避免的会造成某些线程的饥饿.
2.线程处于饥饿是因为不断有优先级高的线程占用资源,当不再有高优先级的线程争抢资源时,饥饿状态将会自动解除
3.产生饥饿的原因:【即线程一直在等待却无法执行的原因】
高优先级线程抢占资源
线程在等待一个本身也处于永久等待完成的对象
线程被永久阻塞在一个等待进入同步块的状态,因为其他线程总是能在他之前持续地对该同步块进行访问(比如阻塞在synchronized)

活锁与死锁的区别

死锁会阻塞,一直等待对方释放资源,一直处在阻塞状态;活锁会不停的改变线程状态尝试获得资源。

活锁有可能自行解开,死锁则不行

sleep 方法与wait方法的区别

sleep() 是 Thread 类的静态本地方法;wait() 是Object类的成员本地方法 sleep()
方法可以在任何地方使用;wait() 方法则只能在同步方法或同步代码块中使用,否则抛出异常Exception in thread
“Thread-0” java.lang.IllegalMonitorStateException sleep()
会休眠当前线程指定时间,释放 CPU 资源,不释放对象锁,休眠时间到自动苏醒继续执行;wait()
方法放弃持有的对象锁,进入等待队列,当该对象被调用 notify() / notifyAll() 方法后才有机会竞争获取对象锁,进入运行状态
JDK1.8 sleep() wait() 均需要捕获 InterruptedException 异常

notify方法与notifyAll的区别

notify() 方法随机唤醒对象的等待池中的一个线程,进入锁池;notifyAll() 唤醒对象的等待池中的所有线程,进入锁池

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值