目录
JUC简介
线程与进程
并行与并发
多线程编程步骤
线程的理解及多线程创建的四种方式
Runnable接口和Callable接口主要区别:
FutureTask类
FutureTask原理:
线程池相关API
比较创建线程的两种方式:
Thread类介绍
线程详解
线程的优先级
线程的生命周期
线程不安全问题
并发编程中的三个问题:
1.原子性
2.可见性
3.有序性
为啥i++不是原子操作?
集合的线程安全问题
(线程同步)解决线程安全问题的三种方式
线程同步过程总的异常问题
线程的死锁问题
synchronized关键字
虚假唤醒(特别重要)
Lock
synchronized和Lock的对比
互斥的概念
互斥经典题目
volatile关键字
锁的介绍
死锁
可重入锁
公平锁和非公平锁
读写锁
悲观锁和乐观锁
Java多线程与操作系统多线程的联系
JUC编程三个强大的辅助类
JUC简介
在Java中,线程部分是一个重点,本篇文章说的JUC也是关于线程的。JUC就是java.util.concurrent工具包的简称。这是一个处理线程的工具包,JDK1.5开始出现的。线程与进程
程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。---生命周期
进程 — 资源分配的最小单位。
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
线程 — 程序执行的最小单位。
线程作为调度和执行的单位,每个线程拥有独立的运行栈和计数器,每个进程拥有独立的方法区和堆;意味着,多个线程共享一个方法区和堆。而共享的就可以优化,同时,共享的也会带来安全隐患,这就需要我们解决线程安全问题
背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需使用多线程呢?
使用多线程的优点:
1.提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
2.提高计算机系统CPU的利用率
3.改善程序结构。将即长又复杂的线程分为多个线程,独立运行,利于理解和修改
何时需要多线程
1.程序需要同时执行两个或多个任务。
2.程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
3.需要一些后台运行的程序时。
并行与并发
并行:进程真的同时在执行(微观角度的同一时刻,是有多个指令在执行的,所以只会在多CPU多核场景下)
并发:进程假的同时在执行(微观上,表现为一次只执行一个进程,但宏观上,多个进程在"同时"执行)
串行模式
并行模式
并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核CPU.
并发
并行并发应用举例(重点):
并发:同一时刻多个线程在访问同一个资源,多个线程对一个点。例如:春运抢票、电商秒杀...
并行:多项工作一起执行,之后再汇总。例如:泡方便面,电水壶烧水,一边撕调料倒入桶中
多线程编程步骤
第一步:创建资源类,在资源类中创建属性和操作方法第二步:在资源中操作方法(判断、干活、通知)第三步:创建多个线程,调用资源类的操作方法单一线程执行流程:
1.创建并启动一个线程
1.继承Thread类,重写run方法
2.实现Runnable接口,重写run方法
2.创建线程实例:构造一个Thread(包括其子类)的对象 —— 控制线程的handle
1.new Thread子类
2.实现Runnable接口,重写run方法
3.启动线程
thread.start()
理解start()做了什么:把线程加入到就绪队列中,等待被调度器选中才能执行
线程的理解及多线程创建的四种方式
方式一:继承于Thread类
1.创建一个继承于Thread类的子类
2.重写Thread类的run()--->将此线程执行的操作声明在run()中
3.创建Thread类的子类的对象
4.通过此对象调用start():start()方法的两个作用:A.启动当前线程 B.调用当前线程的run()
创建过程中的两个问题:
问题一:我们不能通过直接调用run()的方式启动线程
问题二:在启动一个线程,遍历偶数,不可以让已经start()的线程去执行,会报异常;正确的方式是重新创建一个线程的对象。
//1.创建一个继承于Thread类的子类 class MyThread extends Thread{ //2.重写Thread类的run() @Override public void run() {//第二个线程 for(int i = 0;i < 10;i++){ if(i % 2 == 0){ System.out.println(i); } } } } public class ThreadTest { public static void main(String[] args) {//主线程 //3.创建Thread类的子类的对象 MyThread t1 = new MyThread(); //4.通过此对象调用start() t1.start(); //问题一:不能通过直接调用run()的方式启动线程 // t1.run();//错误的 //问题二:再启动一个线程:我们需要再创建 一个对象 //t1.start();//错误的 MyThread t2 = new MyThread(); t2.start(); for(int i = 0;i < 10;i++){ if(i % 2 != 0){ System.out.println(i + "****main()******"); } } } }
此代码在主线程内输出奇数,在另一个线程里输出偶数,则输出结果应该是两个输出结果是交互的。
1****main()****** 3****main()****** 5****main()****** 7****main()****** 0 2 4 6 8 9****main()******
class Window extends Thread{//创建三个窗口卖票, 总票数为100张,使用继承于Thread类的方式 private static int ticket = 100;//三个窗口共享:声明为static @Override public void run() { while(true){ if(ticket > 0){ System.out.println(getName() + ":卖票,票号为:" + ticket); ticket--; }else{ break; } } } } public class WindowTest2 { public static void main(String[] args) { Window t1 = new Window(); Window t2 = new Window(); Window t3 = new Window(); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
public class ThreadDemo { public static void main(String[] args) { // MyThread1 m1 = new MyThread1(); // MyThread2 m2 = new MyThread2(); // m1.start(); // m2.start(); //由于造的类只创建过一次对象,后面就不用了,可以考虑使用匿名类的方式 //创建Thread类的匿名子类的方式 new Thread(){ @Override public void run() { for(int i = 0;i < 100;i++){ if(i % 2 == 0){ System.out.println(i); } } } }.start(); new Thread(){ @Override public void run() { for(int i = 0;i < 100;i++){ if(i % 2 != 0){ System.out.println(i); } } } }.start(); } } class MyThread1 extends Thread{ @Override public void run() { for(int i = 0;i < 100;i++){ if(i % 2 == 0){ System.out.println(i); } } } } class MyThread2 extends Thread{ @Override public void run() { for(int i = 0;i < 100;i++){ if(i % 2 != 0){ System.out.println(i); } } } }
方式二:实现Runnable接口
1.创建一个实现了Runnable接口的类
2.实现类去实现Runnable中的抽象方法:run()
3.创建实现类的对象
4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start()class MThread implements Runnable{ //2.实现类去实现Runnable中的抽象方法:run() @Override public void run() { for(int i = 0;i < 100;i++){ if (i % 2 == 0) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } } public class ThreadTest1 { public static void main(String[] args) { //3.创建实现类的对象 MThread mThread = new MThread(); //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 Thread t1 = new Thread(mThread); t1.setName("线程1"); //5.通过Thread类的对象调用start():A.启动线程B.调用当前线程的run()-->调用了Runnable类型的target t1.start(); //再启动一个线程,遍历100以内的偶数//只需重新实现步骤4,5即可 Thread t2 = new Thread(mThread); t2.setName("线程2"); t2.start(); } }
class window1 implements Runnable{//创建三个窗口卖票, 总票数为100张,使用实现Runnable接口的方式 private int ticket = 100; Object obj = new Object(); @Override public void run() { while (true){ if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket); ticket--; } else { break; } } } } public class WindowTest { public static void main(String[] args) { window1 w = new window1();//只造了一个对象,所以100张票共享 Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("线程1"); t2.setName("线程2"); t3.setName("线程3"); t1.start(); t2.start(); t3.start(); } }
方式三:实现Callable接口---JDK5.0新增
Runnable接口和Callable接口主要区别:
与使用Runnable相比,Callable功能更强大些
(1)是否有返回值
(2)是否抛出异常
(3)实现方法名称不同,一个是run方法,一个是call方法
Callable接口的特点如下(重点)
1. 为了实现Runnable,需要实现不反悔任何内容的run()方法,而对于Callable,需要实现在完成时返回结果的call()方法。
2. call()方法可以引发异常,而run()则不能。
3. 为实现Callable而必须重写call方法。
4. 不能直接替换runnable,因为Thread类的构造方法根本没有Callable
Future接口
1. 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
2. FutureTask是Futrue接口的唯一的实现类
3. FutureTaskb同时实现了Runnable,Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
//1.创建一个实现Callable的实现类 class NumThread implements Callable{ //2.实现call方法,将此线程需要执行的操作声明在call()中 @Override public Object call() throws Exception { int sum = 0; for(int i = 1;i <= 100;i++){ if(i % 2 == 0){ System.out.println(i); sum += i; } } return sum;//sum是int,自动装箱为Integer(Object的子类) } } public class ThreadNew { public static void main(String[] args) { //3.创建Callable接口实现类的对象 NumThread numThread = new NumThread(); //4.将此Callable接口实现类的对象作为参数传递到 FutureTask的构造器中,创建FutureTask的对象 FutureTask futureTask = new FutureTask(numThread); //5.将 FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start() new Thread(futureTask).start(); try { //获取Callable中call()的返回值(不是必须的步骤) //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。 Object sum = futureTask.get(); System.out.println("总和为:" + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
FutureTask类
这个类,即和Runnable有关系,又和Callable也有关系,所以它既可以实现Runnable的作用被线程执行,又可以作为Future得到Callable的返回值。
Runnable接口有实现类FutureTask
FutureTask构造可以传递Callable
Future:封装并行调用的类,可以取消任务的执行,确定执行是否已成功完成或出错,以及其它操作;
FutureTask:是Future接口的实现,将在并行调用中执行。
Callable:用于实现并行执行的接口。它与Runnable接口非常相似,但是它不返回任何值,而Callable必须在执行结束时返回一个值。
可取消的异步计算。此类提供 的基本实现Future,具有启动和取消计算、查询计算是否完成以及检索计算结果的方法。只有在计算完成后才能检索结果;get如果计算尚未完成,这些方法将阻塞。一旦计算完成,就不能重新开始或取消计算(除非使用调用计算runAndReset())。
FutureTask原理:
因为FutureTask实现了Runnable接口,因此FutureTask交由Executor执行,也可以直接用线程调用执行(futureTask.run())。根据FutureTask的run方法执行的时机,FutureTask可以处于以下三种执行状态:
1. 未启动:在FutureTask.run()还没执行之前,FutureTask处于未启动状态。当创建一个FutureTask对象,并且run()方法未执行之前,FutureTask处于未启动状态。
2. 已启动:FutureTask对象的run方法启动并执行的过程中,FutureTask处于已启动状态。
3. 已完成:FutureTask正常执行结束,或者FutureTask执行被取消(FutureTask对象cancel方法),或者FutureTask对象run方法执行抛出异常而导致中断而结束,FutureTask都处于已完成状态。
FutureTask状态迁移图当FutureTask处于未启动或者已启动的状态时,调用FutureTask对象的get方法会将导致调用线程阻塞。当FutureTask处于已完成的状态时,调用FutureTask的get方法会立即放回调用结果或者抛出异常。
当FutureTask处于未启动状态时,调用FutureTask对象的cancel方法将导致线程永远不会被执行;当FutureTask处于已启动状态时,调用FutureTask对象cancel(true)方法将以中断执行此任务的线程的方式来试图停止此任务;当FutureTask处于已启动状态时,调用FutureTask对象cancel(false)方法将不会对正在进行的任务产生任何影响;当FutureTask处于已完成状态时,调用FutureTask对象cancel方法将返回falseFutureTask的get和cancel的执行示意图
当一个线程需要等待另一个线程把某个任务执行完后它才能继续执行,此时可以使用FutureTask。假设有多个线程执行若干任务,每个任务最多只能被执行一次。当多个线程试图同时执行同一个任务时,只允许一个线程执行任务,其他线程需要等待这个任务执行完后才能继续执行。
应用场景:
方式四:使用线程池--->JDK5.0新增
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:>提高响应速度(减少了创建新线程的时间)
>降低资源消耗(重复利用线程池中线程,不需要每次都创建)
>便于线程管理:A.corePoolSize:核心池的大小 B.maximumPoolSize:最大线程数 C.keepAliveTime:线程没有任务时最多保持多长时间后会终止
线程池相关API
class NumberThread implements Runnable{ @Override public void run() { for(int i = 0;i <= 100;i++){ if(i % 2 == 0){ System.out.println(Thread.currentThread().getName() + ":" + i); } } } } class NumberThread1 implements Runnable{ @Override public void run() { for(int i = 0;i <= 100;i++){ if(i % 2 != 0){ System.out.println(Thread.currentThread().getName() + ":" + i); } } } } public class ThreadPool { public static void main(String[] args) { //1.提供指定线程数量的线程池 ExecutorService service = Executors.newFixedThreadPool(10); ThreadPoolExecutor service1 = (ThreadPoolExecutor) service; //设置线程池的属性 // System.out.println(service.getClass()); // service1.setCorePoolSize(15); // service1.setKeepAliveTime(); //2.执行指定的线程操作。需要提供实现Runnable 接口或Callable接口实现类的对象 service.execute(new NumberThread());//适用于Runnable service.execute(new NumberThread1());//适用于Runnable // service.submit(Callable callable);//适用于Callable //3.关闭连接池 service.shutdown(); } }
比较创建线程的两种方式:
开发中:优先选择:实现Runnable接口的方式
原因:1.实现的方式没有类的单继承性的局限性
2.实现的方式更适合来处理多个线程有共享数据的情况。
系:public class Thread implements Runnable
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中
Thread类介绍
可以看到Thread类有很多的构造器,
这个最常用:
public Thread(ThreadGroup group, String name) { init(group, null, name, 0); }
常用方法及测试
1.start():启动当前线程:调用当前线程的run()
2.run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3.currentThread():静态方法,返回执行当前代码的线程
4.getName():获取当前线程的名字
5.setName():设置当前线程的名字
6.yield():释放当前CPU的执行权(下一刻CPU执行的线程仍是随机的)
>暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
>若队列中没有同优先级的线程,忽略此方法
7.join():在线程a中调用线程b的join(),此时,线程a就进入阻塞状态(停止执行),直到线程b完全执行完以后,线程b才结束阻塞状态(开始执行)。
8.sleep(long millitime):让当前线程"睡眠"指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。会抛出InterruptedException异常
9.isAlive():判断当前线程是否存活class HelloThread extends Thread{ @Override public void run() { for(int i = 0;i < 100;i++){ if(i % 2 != 0){ try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" +Thread.currentThread().getPriority() + ":" + i); } } } public HelloThread(String name){ super(name); } } public class ThreadMethodTest { public static void main(String[] args) { HelloThread h1 = new HelloThread("Thread:1");//通过构造器给线程命名,但前期是得在子类中提供一个构造器 // h1.setName("线程一"); //设置分线程的优先级 h1.setPriority(Thread.MAX_PRIORITY); h1.start(); //给主线程命名 Thread.currentThread().setName("主线程"); Thread.currentThread().setPriority(Thread.MIN_PRIORITY); for(int i = 0;i < 100;i++){ if(i % 2 == 0) { System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i); } // if(i == 20){ // try { // h1.join();//join()的测试 // } catch (InterruptedException e) { // e.printStackTrace(); // } // } } } }
线程的优先级
1.
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5--->默认优先级
2.如何获取和设置当前线程的优先级:
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级
说明:高优先级的线程要抢占低优先级线程CPU的执行权,但是只是从概率上讲,高优先级的线程高概率的情况下,不一定被执行,并不意味着只有当高优先级的线程执行完毕后,低优先级的线程才执行。
线程详解
线程的优先级
1.
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5--->默认优先级
2.如何获取和设置当前线程的优先级:
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级
说明:高优先级的线程要抢占低优先级线程CPU的执行权,但是只是从概率上讲,高优先级的线程高概率的情况下,不一定被执行,并不意味着只有当高优先级的线程执行完毕后,低优先级的线程才执行。线程的生命周期
NEW(新建)
RUNNABLE(准备就绪)
BLOCKED(阻塞)
WAITING(不见不散):一直等,等到为止
TIMED_WAITING(过时不候)
TERMINATED(死亡)
wait/sleep的区别:
(1)sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
(2)sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
(3)它们都可以被interrupted方法中断。
线程不安全问题
线程不安全现象出现的原因
1.线程是抢占执行的。
具有随机性,由操作系统内核实现。
2.有的操作不是原子的。当cpu执行一个线程过程时,调度器可能调走CPU,去执行另一个线程,此线程的操作可能还没有结束;(通过锁来解决)
原子性被破坏是线程不安全的最常见的原因!!!
3.多个线程尝试修改同一个变量(一对一修改、多对一读取、多对不同变量修改,是安全的)
需求所要求的。
4.内存可见性
5.指令重排序:java的编译器在编译代码时,会针对指令进行优化,调整指令的先后顺序,保证原有逻辑不变的情况下,来提高程序的运行效率。
即使在多线程的代码中,那些情况下不需要考虑线程安全问题?
1.几个线程之间互相没有任何数据共享的情况下,天生是线程安全的;
2.几个线程之间即使有共享数据,但都是做读操作,没有写操作时,也是天生线程安全的。
并发编程中的三个问题:
1.原子性
即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
下面四个操作,有哪个几个是原子操作,那几个不是?其实只有1才是原子操作,其余均不是。
i = 0; //1 j = i ; //2 i++; //3 i = j + 1; //4 1在Java中,对基本数据类型的变量和赋值操作都是原子性操作; 2中包含了两个操作:读取i,将i值赋值给j 3中包含了三个操作:读取i值、i + 1 、将+1结果赋值给i; 4中同三一样
在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同,Java只保证了基本数据类型的变量和赋值操作才是原子性的(注:在32位的JDK环境下,对64位数据的读取不是原子性操作*,如long、double)。
Java中的原子性操作包括:
(1)基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作。
(2)所有引用reference的赋值操作
(3)java.concurrent.Atomic.* 包中所有类的一切操作
要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。volatile是无法保证复合操作的原子性。
最常见的违反原子性的场景:
1.read - write场景:
i++; array[size] = e; size++;
2.check - update场景
if (a == 10){ a = a + 1; }
2.可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程是否能够立即看得到修改的值。
在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。
对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
3.有序性
即程序执行的顺序按照代码的先后顺序执行
指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
参考博客:
并发编程——原子性,可见性和有序性_eff666的博客-CSDN博客_并发编程 有序性
为啥i++不是原子操作?
Java代码(高级语言)中的一条语句,很可能对应多条指令;r++实质上就是r = r + 1
变成指令动作主要三步:
1.从内存中(r代表的内存区域)把数据加载到寄存器中
2.完成数据加1的操作
3.把寄存器中的值,写回到内存中
2.线程调度是可能发生在任意时刻的,但是不会切割指令(一条指令只有执行完/完全没有执行两种可能)
单看这3条指令,则线程调度可能在4个位置发生都有可能。
程序员的预期是r++或者r--是一个原子性的操作(全部完成 or 全部没完成)
但实际执行起来,保证不了原子性。
如何实现i++的原子性?
1.使用juc中的lock
2.使用Java关键字synchronized
3.使用juc中的AtomicInteger中的getAndIncrement()
4.volatile并不能保证原子性操作
集合的线程安全问题
之前学过的一些常见类的线程安全问题
ArrayList、LinkedList、PriorityQueue、TreeMap、TreeSet、HashMap、HashSet、StringBuilder都不是线程安全的
ArrayList为什么不是线程安全的?—— 多个线程同时对一个ArrayList对象有修改操作时,结果会出错。
public boolean add(long e){ //以下操作应该是原子的,但目前不是 array[size] = e; size++; return true; }
(线程同步)解决线程安全问题的三种方式
三个窗口卖票的例子解决线程安全问题
1.问题:买票过程中,出现了重票、错票-->出现了线程的安全问题
2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票
3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来,知道线程a操作完ticket时,其他
线程才可以开始操作ticket,这种情况即使线程a出现了阻塞,也不能被改变
4.在Java中,我们通过同步机制,来解决线程的安全问题。(线程安全问题的前提:有共享数据)锁的本质:
锁理论上,就是一段数据(一段被多个线程之间共享的数据)
在加锁至解锁的这段代码里,即加锁的内部操作:
整个尝试加锁的操作已经被JVM保证了原子性
一个要知道的现象
语句1;
sync(ref){语句2;...}
语句1的执行时间和语句2 的执行时间相隔很久;甚至极端情况下,语句2再也不会被执行都有可能。
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码(或操作共享数据的代码)
}
说明:1.操作共享数据的代码,即为需要被同步的代码(不能包含代码多了(变成单线程会效率低,也有可能会出错),也不能包含代码少了(没包的会阻塞))
2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据
3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。(特别注意!!!!!)
补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用(具体问题具体分析)this充当同步监视器class window1 implements Runnable{ private int ticket = 100; // Object obj = new Object(); @Override public void run() { while (true){ synchronized (this) {//此时的this:唯一的Window1的对象 // synchronized(obj) { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket); ticket--; } else { break; } } } } } public class WindowTest1 { public static void main(String[] args) { window1 w = new window1();//只造了一个对象,所以100张票共享 Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("线程1"); t2.setName("线程2"); t3.setName("线程3"); t1.start(); t2.start(); t3.start(); } }
class Window extends Thread{ private static int ticket = 100;//三个窗口共享:声明为static private static Object obj = new Object(); @Override public void run() { while(true) { // synchronized (obj) { synchronized (Window.class){//Class clazz = Window.class,Window.class只会加载一次 // synchronized (this) {//错误的方式:this代表着t1,t2,t3三个对象 if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + ":卖票,票号为:" + ticket); ticket--; } else { break; } } } } } public class WindowTest { public static void main(String[] args) { Window t1 = new Window(); Window t2 = new Window(); Window t3 = new Window(); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
4.同步的方式,解决了线程的安全问题。---->好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。--->局限性关于同步方法的总结:
1.同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
2.非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身class window3 implements Runnable{ private int ticket = 100; @Override public void run() { while (true){ show(); } } public synchronized void show(){//同步监视器:this(未显示声明而已) if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket); ticket--; } } } public class WindowTest3 { public static void main(String[] args) { window3 w = new window3(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("线程1"); t2.setName("线程2"); t3.setName("线程3"); t1.start(); t2.start(); t3.start(); } }
class Window4 extends Thread{ private static int ticket = 100;//三个窗口共享:声明为static @Override public void run() { while(true){ show(); } } private static synchronized void show() {//同步监视器:Window4.class(类) // private synchronized void show() {//同步监视器:t1,t2,t3。此种解决方式是错误的 if(ticket > 0){ System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } } } public class WindowTest4 { public static void main(String[] args) { Window4 t1 = new Window4(); Window4 t2 = new Window4(); Window4 t3 = new Window4(); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
方式三:Lock锁---JDK5.0新增
JDK5.0开始,Java提供了更强大的线程同步机制---通过显式定义同步锁对象来实现同步,同步锁使用Lock对象充当。
java.util.concurrent.locks接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁。
class Window implements Runnable{ private int ticket = 100; //1.实例化ReentrantLock private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true){ try{ //2.调用锁定方法:lock() lock.lock(); if(ticket > 0){ try { Thread.sleep(100); }catch (InterruptedException e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket); ticket--; }else{ break; } } finally{ //3.调用解锁方法:unlock(); lock.unlock(); } } } } public class LockTest { public static void main(String[] args) { Window w = new Window(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
线程同步过程总的异常问题
JVM在设计对象时,在对象内部都有一个monitor(监视器),每个对象都有!!!!
JVM就可以把每个对象都当成锁来使用。(回头看,这个设计不是太好)
synchronized(ref){...} 当ref == null的时候,结果会如何?
隐含着一个解引用(dereference)的操作(通过引用操作引用指向的对象)
如果null,一定会有NullPointerException
线程的死锁问题
1.死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己的需要
的同步资源,就形成了线程的死锁。
2.说明:
>出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
>我们使用同步时,要避免出现死锁3.解决方法
A.专门的算法、原则 B.尽量减少同步资源的定义 C.尽量避免嵌套同步
public class ThreadTest { public static void main(String[] args) { StringBuffer s1 = new StringBuffer(); StringBuffer s2 = new StringBuffer(); new Thread(){//匿名的方式继承 @Override public void run() { synchronized(s1){ s1.append("a"); s2.append("1"); try { Thread.sleep(100);//增加死锁出现概率 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2){ s1.append("b"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }.start(); new Thread(new Runnable(){//匿名的方式实现Runnable接口 @Override public void run() { synchronized (s2){ s1.append("c"); s2.append("3"); try { Thread.sleep(100);//增加死锁出现概率 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s1){ s1.append("d"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } } }).start(); } }
synchronized关键字
synchronized加锁操作的作用:
1.原子性(90%):通过将需要保证原子性的操作互斥起来
2.可见性(5%)
3.重排序(5%)
synchronized保证原子性!!!
通过正确地加锁使得应该原子性的代码之间互斥来实现!!!!
synchronized在有限程度上可以保证内存可见性
synchronized也可以给代码重排序增加一定的约束
关于synchronized更深入的理解看这里:
Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级_tongdanping的博客-CSDN博客_synchronized锁升级
深入理解Java并发之synchronized实现原理_zejian_的博客-CSDN博客_synchronized原理
虚假唤醒(特别重要)
Object()类中的wait()方法
因为if()条件成立,执行wait(),但当再次唤醒后,会顺序执行下面的语句,不会再进行if()判断,即可能出现错误,所以这样会产生虚假唤醒的问题
解决方式
while(number != 0){ this.wait();//不管啥时候醒,都要再次执行判断 }
Lock
Lock锁实现提供了比同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock提供了比synchronized更多的功能。
synchronized和Lock的对比
1.Lock是显示锁(手动开启和关闭锁,别忘记关闭锁,如果没有主动释放锁,可能出现死锁现象),synchronized是隐式锁,出了作用域自动释放
2.Lock只有代码块锁,synchronized有代码块锁和方法锁
3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的拓展性(提供更多的子类)
将unlock放到finally里,确保任何情况下都能解锁
try{ if(number > 0){ } } finally{ lock.unlock();//手动解锁 }
4.synchronized 与 Lock的异同?
相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器 Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
优先使用顺序: Lock ->同步代码块(已经进入了方法体,分配了相应资源) ->同步方法(在方法体之外)
互斥的概念
当多线程相互竞争操作共享变量时,由于运气不好,即在执行过程中发生了上下文切换,我们得到了错误的结果,事实上,每次运行都可能得到不同的结果,因此输出的结果存在不确定性(indeterminate)。
由于多线程执行操作共享变量的这段代码可能会导致竞争状态,因此将此段代码称为临界区(critical section),它是访问共享资源的代码片段,一定不能给多线程同时执行。
我们希望这段代码是互斥(mutualexclusion)的,也就说保证一个线程在临界区执行时,其他线程应该被阻止进入临界区,说白了,就是这段代码执行过程中,最多只能出现一个线程可以持有锁(加锁成功),其余加锁失败的线程都会被:
1.进入该锁的阻塞队列(等待队列)
2.放弃CPU(运行 -> 阻塞)
另外,说一下互斥也并不是只针对多线程。在多进程竞争共享资源的时候,也同样是可以使用互斥的方式来避免资源竞争造成的资源混乱。 因为:临界区的代码是必须在持有锁的前提下才能执行的。
多个线程为了同个资源打起架来了,该如何让他们安分?
互斥经典题目
判断以下那些会互斥,哪些不会
public class Test { public static void main(String[] args) { SomeClass s1 = new SomeClass(); SomeClass s2 = new SomeClass(); SomeClass s3 = s1; } } class SomeClass{ synchronized void m1(){} synchronized static void m2(){} void m3(){} void m4(){ synchronized (this){} } void m5(){ synchronized (SomeClass.class){} } Object o1 = new Object(); void m6(){ synchronized (o1){} } static Object o2 = new Object(); void m7(){ synchronized (o2){} } }
假设现在有t1,t2两个线程
一旦正确加锁:多个线程都尝试加锁 && 请求的是同一把锁时,会出现互斥现象
某一时刻,只有一个线程(请求到锁的线程)在运行临界区指令
其它线程(请求锁失败的线程) 在等待
互斥的必要条件:线程都有加锁操作 && 不同的线程加的锁是同一把锁(同一个对象)
volatile关键字
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
简单概括volatile,它能够使变量在值发生改变时能尽快地让编译器知道并告知其他线程,不要对此变量进行优化,每次都要到变量的地址中去读取变量的数据。volatile仅能实现变量的修改可见性;volatile仅能使用在变量级别
volatile变量的特性
1.保证可见性,不保证原子性
(1)当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;(2)这个写会操作会导致其他线程中的volatile变量缓存无效。
2.禁止指令重排
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:(1)重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
(2)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。
class TestVolatile{ int a = 1; boolean status = false; //状态切换为true // public void changeStatus(){ // a = 2; // status = true; // } //若状态为true,则为running public void run(){ if(status){ int b = a + 1; System.out.println(b); } } }
使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:
a.当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
b.在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。
volatile原理
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(2)它会强制将对缓存的修改操作立即写入主存;
(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
volatile关键字能修饰在哪些地方?
volatile关键字可以修饰在类变量或者实例变量上,不能修饰在方法参数,局部变量,实例常量以及类常量上。
好的博客:
volatile和synchronized的区别_我是陈旭原的博客-CSDN博客_volatile和synchronized
Java volatile关键字最全总结:原理剖析与实例讲解(简单易懂)_老鼠只爱大米的博客-CSDN博客_java volatile
volatile修饰的变量_volatile 关键字,你真的理解吗?_weixin_39945679的博客-CSDN博客
锁的介绍
多线程访问共享资源的时候,避免不了资源竞争而导致数据错乱的问题,所以我们通常为了解决这一问题,都会在访问共享资源之前加锁。
最常用的就是互斥锁,当然还有很多种不同的锁,比如可重入锁、自旋锁、读写锁、乐观锁等,不同种类的锁自然适用于不同的场景。
如果选择了错误的锁,那么在一些高并发的场景下,可能会降低系统的性能,这样用户体验就会非常差了。
所以,为了选择合适的锁,不仅需要清楚知道加锁的成本开销有多大,还需要分析业务场景中访问的共享资源的方式,再来还要考虑并发访问共享资源时的冲突概率。
对症下药,才能减少锁对高并发性能的影响。
接下来,针对不同的应用场景,谈一谈各种锁的选择和使用。
死锁
死锁:两个或两个以上进程在执行过程中,因为争夺资源而造成一种互相等待的现象,如果没有外力干涉,他们无法再执行下去。
显然两个线程都在等待对方释放锁,最终进入了死锁状态。一旦出现死锁,整个程序既不会发生任何错误,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。Java虚拟机没有提供检测,也没有采取任何措施来处理死锁的情况,所以多线程编程中,必须手动采取措施避免死锁。
产生死锁的原因:
1.系统资源不足
2.进程推进顺序不合适;
3.资源分配不当;
死锁的四个必要条件:
1.互斥条件:一个资源每次只能被一个线程使用。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
解决死锁问题:
只要破坏死锁四个必要条件中的任何一个或多个,死锁问题就能被解决。
1.避免死锁的发生:
A. 如果其他对象的这个方法会消耗比较长的时间,那么就会导致锁被持有了很长时间;
B. 如果其它对象的这个方法是一个同步方法,那么就要注意避免发生死锁的可能性了;
总之,尽量避免在同一个方法中调用其它对象的延时方法和同步方法。
2.加锁顺序:能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生
3.加锁时限:线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁。
可重入锁
广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个锁),这样的锁叫做可重入锁。可重入锁的作用就是为了避免死锁。
ReentrantLock(显式的Lock锁)和synchronized(隐式)都是可重入锁。
使用ReentrantLock的注意点
ReentrantLock和synchronized不一样,需要手动释放锁,所以使用ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要一样,如果加锁和释放次数不一样会导致死锁。
公平锁和非公平锁
在某些业务场景中会需要保证先到的线程先得到锁,所以就有了公平锁和非公平锁的诞生。
公平锁:加锁时考虑排队等待问题,按照申请锁的顺序,按照FIFO规则,先申请的线程先取得锁,其他线程进入队列等待锁的释放,当锁释放后,在队头的线程被唤醒。
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:加锁时不考虑排队等待问题,直接尝试获取锁。如果此时恰好锁处于unlock,则不管有没有其他线程在等待,直接拿到锁;否则就转化成公平锁的模式,进入队列等待。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必去唤醒所有线程,会减少唤起线程的数量。
缺点:可能会导致在阻塞队列中的线程长期处于饥饿状态或者说很早就在等待锁,但要等很久才会获得锁。
两者对比:非公平锁性能比公平锁高5~10倍,因为公平锁需要频繁唤醒队列中的线程,比较消耗资源,非公平锁效率比较高,但是非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态或者说很早就在等待锁,但要等很久才会获得锁。
其中的原因是公平锁是严格按照请求锁的顺序来排队获得锁的,而非公平锁是可以抢占的,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,那么这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。
拓展:synchronized也属于非公平锁。
参考博客:Java中的公平锁和非公平锁实现详解_平菇虾饺的博客-CSDN博客_公平锁和非公平锁
公平锁和非公平锁源码分析:
首先,在ReentrantLock类的源码中,有两个子类:FairSync(公平锁)和NofairSync(非公平锁),当一个线程进入ReentrantLock,默认是非公平锁。
//ReentrantLock源码 public ReentrantLock() { sync = new NonfairSync(); }
参考博客:阿里面试官:说一下公平锁和非公平锁的区别?_敖 丙的博客-CSDN博客
在开发的时候,需要自己来考虑和权衡是要用公平策略还是非公平策略。
读写锁
锁的演化:
1)无锁:多线程抢夺资源(乱)
2)添加锁:使用synchronized和ReentrantLock;都是独占的,每次只能来一个操作;读操作可以共享,读写操作不可以共享
3)读写锁:ReentrantReadWriteLock;读操作可以共享,提升性能,同时多人进行读操作;写操作不能共享。
缺点:A. 造成锁饥饿,一直读,没有写操作,比如坐地铁。B.读的时候,不能写,只有读,完成之后,才可以写,写操作,可以读
读写锁的工作原理:
读写锁从字面意思我们也可以知道,它由【读锁(共享锁)】和【写锁(独占锁)】两部分构成,如果只读取共享资源用【读锁】加锁,如果要修改共享资源则用【写锁】加锁。
所以,读写锁适用于能明确区分读操作和写操作的场景。
原理:
当【写锁】没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为【读锁】是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。
但是,一旦【写锁】被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。
所以说,写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有。知道了读写锁的工作原理后,可以发现,读写锁在读多写少的场景,能发挥出优势。
另外,根据实现的不同,读写锁可以分为【读优先锁】和【写优先锁】。
读优先锁期望的是,读锁能被更多的线程持有,以便提高读线程的并发性,它的工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取写锁。如下图:
而写优先锁是优先服务写线程,其工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取读锁。如下图:
读优先锁对于读线程并发性更好,但也不是没有问题。试想一下,如果一直有读线程获取读锁,那么写线程将永远获取不到写锁,这就造成了写线程【饥饿】的现象。
写优先锁可以保证写线程不会饿死,但是如果一直有写线程获取写锁,读线程也会被【饿死】。
既然不管优先读锁还是写锁,对方可能会出现饿死问题,那么就不偏袒任何一方,搞个【公平读写锁】。
公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现【饥饿】的现象。
互斥锁和自旋锁都是最基本的锁,读写锁可以根据场景来选择这两种锁其中的一个进行实现。
读写锁重要内容概述:
ReetrantReadWriteLock读锁使用共享模式,即:同时可以有多个线程并发地读数据。
但是每次只能有一个写线程。ReadWriteLock适用于读多写少的并发情况。
一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容,读写锁之间为互斥。
1.Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性
2.ReetrantReadWriteLock读写锁的效率明显高于synchronized关键字
3.ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的
4.ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁。锁降级(JDK8) :
将写入锁降级为读锁
读锁不能升级为写锁
源码分析:
Java并发包中ReadWriteLock是一个接口,主要有两个方法,如下:
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing */ Lock writeLock(); }
ReentrantReadWriteLock有如下特性:
获取顺序
1)非公平模式(默认)
当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。
2)公平模式
当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。
当有写线程持有写锁或者有等待的写线程时,一个尝试获取公平的读锁(非重入)的线程就会阻塞。这个线程直到等待时间最长的写锁获得锁后并释放掉锁后才能获取到读锁。
可重入
允许读锁可写锁可重入。写锁可以获得读锁,读锁不能获得写锁。
锁降级
允许写锁降低为读锁
中断锁的获取
在读锁和写锁的获取过程中支持中断
支持Condition
写锁提供Condition实现
监控
提供确定锁是否被持有等辅助方法Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
ReentrantReadWriteLock可以用来提高某些集合的并发性能。当集合比较大,并且读比写频繁时,可以使用该类
构造方法
public ReentrantReadWriteLock() { this(false); } public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); }
可以看到,默认的构造方法使用的是非公平模式,创建的Sync是NonfairSync对象,然后初始化读锁和写锁。一旦初始化后,ReadWriteLock接口中的两个方法就有返回值了,如下:
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; } public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
从上面可以看到,构造方法决定了Sync是FairSync还是NonfairSync。Sync继承了AbstractQueuedSynchronizer,而Sync是一个抽象类,NonfairSync和FairSync继承了Sync,并重写了其中的抽象方法。
好的博客:
深入理解读写锁—ReadWriteLock源码分析_xingfeng_coder的博客-CSDN博客_读写锁
悲观锁和乐观锁
悲观锁
每次去拿数据的时候都认为别的线程会修改。所以每次在拿数据的时候都会上锁。这样别的线程想拿数据就被挡住,直到悲观锁被释放,悲观锁中的共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
但是在效率方面,处理加锁的机制会产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,如果已经锁定了一个线程A,其他线程就必须等待该线程A处理完才可以处理。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中
synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证
乐观锁
每次去拿数据的时候都认为别的线程不会修改。所以不会上锁,但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别的线程有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作),乐观锁适用于多读的应用类型,这样可以提高吞吐量
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本(version)或者是时间戳来实现,不过使用版本记录是最常用的。
乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中
java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。乐观控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
两种锁的应用场景
悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
总结:两种所各有优缺点,读取频繁使用乐观锁,写入频繁使用悲观锁。
Java多线程与操作系统多线程的联系
首先两者的线程状态是一样的。(创建、就绪、执行、阻塞、终止),其实这五个状态也是进程的状态。
在Java代码中看到的线程状态(只能获取不能设置,状态的变更是JVM控制的)
OS中的线程实现:
可以将OS实现线程的方式分为两类,一类是用户级,一类是内核级。这两类的不同在于用户级线程是在用户空间实现的,而内核级线程是在OS内核空间实现的。设置用户级线程的系统,调度是以进程为单位的。而设置了内核级进程的而是以线程为单位进行调度的。
OS内核常驻在内存中,所以将内存空间分为内核空间和用户空间。 线程在内核中实现有以下几个好处:
1. 内核可以在多处理器系统中调度同一进程中的多个线程并行执行。
2. 如果某一个进程中的线程阻塞了,可以跨进程调度其他线程进行执行
3. 线程的切换速度快,开销小。并且内核本身支持多线程技术可以提高系统的执行速度和效率。
但是也有个缺点,当用户将线程交付下来时要将线程由用户态切换为核心态再进行调度等操作,因此也会造成模式切换的开销较大。
线程在用户空间实现的主要优点:
1. 首先线程的切换不需要转换到内核空间,所以节省了模式切换的开销
2. 用户级线程的实现和OS平台无关,对于线程管理的代码是属于用户程序的一部分。用户级线程甚至可以在不支持线程机制的OS上实现。
用户级线程的缺点:
1. 一个进程被分配一个CPU,同一时刻只能有一个线程运行。
2. 若进程中的正在运行的线程阻塞,则当前进程的其他线程全被阻塞。
组合方式: 组合方式综合前两种的优点,分为三种模型:多对一、一对一、多对多 简单理解下就是 不同数量的用户级线程搭配Java中的多线程实现
看到用户级的线程实现,是不是都要以为用户级的线程就是我们现在Java的多线程支持,然而并不是。Java分为两种线程:用户线程和守护线程。
用户线程:
不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度、和管理线程的函数来控制用户线程。用户程序自定义线程
守护线程:
是一个服务线程,准确的来说就是服务用户线程。比如垃圾回收
总结:
二者基本上是一样的,唯一的区别在于JVM何时离开。
用户线程:当存在任何一个用户线程未结束,JVM是不会结束的。
守护线程:如果只剩下守护线程未结束,JVM是可以结束的;可以说守护线程是依赖于用户线程。
默认情况下启动的线程是用户线程,通过setDaemon(true)将线程设置为守护线程,这个方法必须在线程启动前进行调用,否则会报IllegalThreadStateException异常,启动的线程无法变成守护线程,而是用户线程。
JVM进程什么时候才能退出:
1.必须要求所有前台都退出,和主线程没关系2.和后台线程没关系,即使后台线程还在工作,也正常退出所有的前台线程都退出了,JVM进程就退出了Java多线程与操作系统线程
Java多线程是在JVM中实现的,而JVM相对于OS是一个进程。我们用Java编写的多线程程序对于OS来说是不可见的。OS只关心JVM交付给它的任务。至于我们写的多线程具体是怎么实现的,就要看JVM怎么将我们的程序交给OS运行了。
特别注意:线程的状态是JVM中的线程状态!不是操作系统中的线程状态。
多个线程为了同个资源打起架来了,该如何让他们安分?面试官:你说说互斥锁、自旋锁、读写锁、悲观锁、乐观锁的应用场景-CSDN博客_自旋锁和互斥锁的使用场景
JUC编程三个强大的辅助类
1.减少计数CountDownLatch
场景:6个同学陆续离开教室后值班同学才可以关门。
2.循环栅栏CyclicBarrier
3.信号灯Semaphore
总结:辅助类只有当对线程的理解达到一定深度时,才会更有用。