首页 > 编程语言 >Java并发

Java并发

时间:2023-06-08 14:22:41浏览次数:52  
标签:Java synchronized Thread 对象 并发 线程 run 执行

本系列参考自Java面试小抄以及黑马程序员

线程创建

创建线程的方式

  1. Runnable或Callable接口。新建类时实现Runnable接口,然后在Thread类的构造函数中传入MyRunnable的实例对象,最后执行start()方法。
  2. 继承Thread类,重写run()
  3. lambda精简代码:Runnable接口中只有一个抽象化方法且被@FunctionalInterface修饰,这种接口就可以用lambda简化。

lambda是一个匿名函数的简写形式。(参数列表)->{代码};

class MyThread extends Thread {
	@Override
	public void run() {
		System.out.println("My thread is running!");
	}
}
class MyRunnable implements Runnable{
	@Override
	public void run() {
		System.out.println("My Runnable is running!");
	}
}
public class Solution {
	public static void main(String[] args) {
		Thread t = new MyThread();
		t.start();
		Runnable r1 = new MyRunnable();
		Thread t1 = new Thread(r1,tt1);//线程t1创建新的线程tt1
		t1.start();
		Runnable r1 = new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				System.out.println("Running");
			}
		};
		Runnable r2 = () -> {System.out.println("Running");};
		Thread t2 = new Thread(r1);
		t2.start();
		
	}
}

Runnable和Callable区别:

Callable规定重写的方法是call(),Runnable规定的是run()
callable的任务执行后可返回值,Runnable的任务不能返回值
call()可以抛出异常,run()不行
运行Callable任务可以拿到一个Future对象,表示异步计算的结果,提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况。

start()和run()

调用start()会执行线程的相应准备工作,然后自动执行run()的内容,并不会等待run()的返回,而是直接继续往下运行。也就是说JVM会另起一条线程执行run()方法,起到多线程的效果。
直接运行run(),会将其当作main线程下的普通方法执行,并不会在某个线程中执行它,还是在主线程里执行。

join()

因为start()不等待run()的返回,所以如果run()中有对于数据的操作而没有及时返回的话,start()的取值可能并不正确。t.join():主线程等待t线程运行结束。

线程状态[[进程与线程1#线程的状态转换|相关]]

新建状态:Thread t = new MyThread();
就绪状态:t.start()
运行状态:CPU开始调度就绪状态的线程
阻塞状态:运行状态的线程执行wait()、线程在获取synchronized同步锁失败、线程的sleep()或I/O阻塞。
死亡状态:线程执行完毕或因异常退出了run()。

shutdown()和shutdownNow()的区别
shutdown():关闭线程池,线程池状态为SHUTDOWN,不会再接受新任务,但是队列里的任务得执行完毕。
shutdownNow():关闭线程池,线程池状态为STOP,终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的List。

线程阻塞和死亡

sleep()和wait(),yield()

sleep():Thread类的静态方法。当前进程将睡眠n毫秒,线程进入阻塞状态。时间到了解除阻塞,进入可运行状态,等待CPU的调度。
wait():Object方法,必须与synchronized关键字一起使用,线程进入阻塞状态。当notify或notifyall被调用后,解除阻塞。只有重新占用互斥锁之后才会进入可运行状态。线程不会自动苏醒,需要别的线程调用同一个对象上的notify方法。
yield():暂停当前正在执行的线程对象,让其他有相同优先级的线程执行。只能保证当前线程放弃CPU占用而不能保证其他线程一定能占用CPU。

三种阻塞情况

  1. 等待阻塞:运行状态的线程执行wait()方法后,JVM将线程放入等待序列。
  2. 同步阻塞:运行状态的线程在获取对象的同步锁时,若该同步锁被其他线程占用,则JVM将其放入锁池中。
  3. 其他阻塞:运行状态的线程执行Thread.sleep()Thread.join()方法,或发出I/O请求时,JVM会将该线程设置为阻塞状态。

死亡的三种方式

  1. 正常结束:线程执行完run()call()
  2. 异常结束:线程抛出一个未捕获的Exception或Error
  3. 调用stop():不推荐使用,易导致死锁。

线程安全

对临界资源的竞争。

synchronized

阻塞方式,即用对象锁保证了临界区内代码的原子性。采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程想获得该对象锁时就会被阻塞。必须等执行完synchronized的代码,获得对象锁的线程才会释放锁,并唤醒被阻塞的线程。synchronized会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证操作的内存可见性。

synchronized用法

  • 修饰方法:
    成员方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
    静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁。
  • 修饰代码块:作用于当前对象实例,指定加锁对象,对给定对象加锁。
//面向过程
public class Solution {
	static int counter = 0;
	static Object obj = new Object();
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(() -> {
			for (int i = 0; i < 5000; i++) {
				synchronized (obj) {
					counter++;
				}
			}
		}, "t3");
		Thread t2 = new Thread(() -> {
			for (int i = 0; i < 5000; i++) {
				synchronized (obj) {
					counter--;
				}
			}
		}, "t4");
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println("counter:" + counter);
	}
}
//面向对象
class Room {
	private int counter = 0;
	public void increment() {
		synchronized (this) {
			counter++;
		}
	}
	//或将关键词写在方法上
	public synchronized void decrement() {
			counter--;
	}

	public int getCounter() {
		synchronized (this) {
			return counter;
		}
	}
}
//main函数
public static void main(String[] args) throws InterruptedException {
	Room room = new Room();
	Thread t1 = new Thread(() -> {
		for (int i = 0; i < 5000; i++) {
			room.increment();
		}
	}, "t3");
	Thread t2 = new Thread(() -> {
		for (int i = 0; i < 5000; i++) {
			room.decrement();
		}
	}, "t4");
	t1.start();
	t2.start();
	t1.join();
	t2.join();
	System.out.println("counter:" + room.getCounter());
}

synchronized和[[JVM#JMM|volatile]] 的区别

volatile仅能使用在变量级别,synchronized可以修饰变量、方法和类。
volatile仅能保证变量的修改可见性,不能保证原子性。而synchronized可以保证变量修改可见性和原子性
volatile不会造成线程的阻塞,本质是告诉JVM当前变量在工作内存中的值是不确定的,需要从主存中读取。synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞。
volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

synchronized底层实现原理

Monitor(锁/[[进程与线程2#管程|管程]])。synchronized 同步代码块的实现是对应字节码中的 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权。(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)
其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的再执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取而代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
Pasted image 20230606194348.png
Pasted image 20230606194239.png
Pasted image 20230606195238.png

轻量级锁与重量级锁

如果一个对象虽然有多个线程访问,但多线程访问的时间是错开的,就可以使用轻量级锁来优化。语法仍然是synchronized,即先调用轻量级锁,如果失败再调用重量级锁。

  • 创建锁记录(lock record)对象,每个线程的栈帧包含一个锁记录结构,内部存储锁定对象的MarkWord。
  • 让锁记录的object reference指向所对象,并尝试用cas替换object的markword,将markword值存入锁记录。
  • 如果cas替换成功,对象头存储所记录地址和状态00,表示由该线程给对象加锁。
  • 如果cas失败:
    • 如果其他线程已持有该object的轻量级锁,表示有竞争,进入锁膨胀。
    • 如果自己执行了synchronized锁重入,那么再添加一条lock record作为重入的计数。
      Pasted image 20230607094549.png
  • 当退出synchronized代码块,如果有取值为null的锁记录,表示有重入,这是重置锁记录,表示重入计数减一。如果不为null,使用cas将markword的值恢复给对象头,成功则解锁成功,失败则说明轻量级锁进行了锁膨胀或升级成重量级锁。

CAS锁

Compare and Swap,比较并交换,是一条CPU同步原语,是一种硬件对并发的支持,用于管理对共享数据的并发访问。
当且仅当需要读写的内存值V等于旧的预期值A时,CAS通过原子方式用新值B来更新V,否则不会执行任何操作。
CAS的缺陷:

  • ABA问题:只能判断共享变量是否一致,但无法获知共享变量是否被修改过。假设初始条件是A,修改数据时发现是A就会进行修改。但是看到的虽然是A,中间可能发生了A变为B再变为A的情况,数据即使成功修改,也可能有问题。
  • 循环时间长开销:自旋CAS,如果一直循环执行,会给CPU造成风长达的执行开销。
  • 只能保证一个变量的原子操作:如果对多个变量操作,CAS无法保证操作的原子性。

偏向锁

轻量级锁在没有竞争时,每次重入仍然需要执行cas操作,产生锁记录。因此引入偏向锁优化,只有第一次调用synchronized使用cas,减少同一线程获取锁的代价。
如果开启偏向锁(默认开启),markword后三位为101,此时thread、epoch、age都为0。偏向锁默认延迟,不会在程序启动时立即生效,除非加入VM参数-xx:BiasedLockingStartupDelay=0禁用延迟。解锁后该锁偏向于该线程,所以并不会使用cas换回原来的值,而是保持不变。
VM参数-xx:-UseBiasedLocking禁用偏向锁。或者使用代码对象.hashCode();撤销对象的偏向状态。或多个线程访问同一个锁,撤销偏向锁后会将偏向锁升级为轻量级锁。即markword后三位从101->000->001。

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID。
当撤销偏向锁阈值超过20次后,jvm会给这些对象加锁时重新偏向至加锁线程。
当撤销偏向锁阈值超过40次后,JVM会认为根本不该偏向,所以整个类的所有对象都会变为不可偏向,新建的对象也是不可偏向的。

锁膨胀

线程1加轻量级锁失败,进入锁膨胀流程:

  • 为object对象申请monitor锁,让object指向重量级锁地址。
  • 自己进入monitor的entrylist blocked中
    Pasted image 20230607095621.png
    当线程0解锁时,使用cas将markword值恢复给对象头,失败。进入重量级解锁流程,按照monitor地址找到monitor对象,设置owner=null,唤醒entrylist中blocked线程
    膨胀方向:偏向锁->轻量级锁->重量级锁。且膨胀方向不可逆。

自旋优化

重量级锁竞争时,如果当前线程自旋成功,即这时持锁线程已经释放了锁,当前线程就可以避免阻塞。(自旋会占用CPU时间,多核CPU才有意义)

体现了synchronized是非公平锁:

  1. 当持有锁线程释放锁时,a)先将锁的持有者owner赋null,b)然后唤醒等待链表中的一个线程。如果有其他线程刚好在尝试获取锁(比如自旋),则可以马上获得锁。(来的早不如来的巧)
  2. 当线程尝试获取锁失败进入阻塞时,放入链表的顺序和最终被唤醒的顺序是不一致的。

锁消除优化

在JIT编译时,丢运行上下文进行扫描,去除不可额能存在竞争的锁,提高运行效率,因为加锁过程很耗时耗力。锁消除功能默认打开,如果需要关闭,需要通过java参数-XX:-EliminateLocks

线程池

使用线程池的好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  • 提高响应速度。
  • 提高线程的可管理性。使用线程池可以进行统一的分配、调优和监控。

execute()和submit()的区别

execute()用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否
submit()用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过该对象可以判断任务是否执行成功,同时也可以通过该对象来获取返回值。

线程池核心参数

  • corePoolSize:核心线程大小,正在运行的线程个数
  • maximumPoolSize:线程池最大线程数=核心线程+救急线程
  • keepAliveTime:救急线程的心跳时间,如果救急线程在此时间内没有运行任务,该线程消亡。
  • unit:时间单位——针对救急线程
  • workQueue:阻塞队列。
  • handler:饱和策略。当线程池满了后需要执行的策略。通过RejectedExecutionHandler接口实现。
    • AbortPolicy(默认):让调用者抛出RejectedExecutionException异常
    • CallerRunsPolicy:让调用者运行任务
    • DiscardPolicy:放弃本次任务
    • DiscardOldestPolicy:放弃队列中最早的任务,本任务取而代之
    • ...
    • Dubbo:抛出异常前会记录日志,并dump线程栈的信息
    • Netty:创建一个新线程来执行任务
    • ActiveMQ:带超时等待尝试放入队列
    • PinPoint:使用一个拒绝策略链,逐一尝试策略链中每种拒绝策略。
  • ThreadFactory:线程工厂,可以为线程创建时起个名字

线程池执行任务流程

  1. 线程池执行execute/submit方法向线程池中添加任务,当任务小于核心线程数时,线程池可以创建新的线程
  2. 当任务大于核心线程时,加入阻塞队列中
  3. 如果阻塞队列已满,需要通过比较是否小于线程池最大线程数,当小于则创建救急线程,当大于则执行饱和策略。

一些工厂线程池

  • newFixedThreadPool:创建一个指定工作线程数量的线程池。只有核心线程没有救急线程。阻塞队列是无界的,可以放任意数量的任务。
  • newCachedThreadPool:核心线程数为0,救急线程的空闲生存时间是60s。意味着创建的线程全部都可以回收,且无限创建。队列采用了SynchronousQueue,特点是没有容量,没有线程取则放不进去(一手交钱,一手交货)
  • newSingleThreadPool:希望多任务排队执行,线程固定数为1。
    • 单线程与单线程池的区别:如果任务执行出现异常,单线程则直接退出了,而线程池还会新建一个线程,保证池的正常工作。
    • 单线程池和固定大小为1的线程池的区别:固定大小线程池初始为1,以后还可以修改,而单线程池线程个数始终为1,不能修改。

源码中线程池如何复用线程

源码中ThreadPoolExecutor中有一个内置对象Worker,每个worker都是一个线程,worker线程数量和参数有关,每个worker会从阻塞队列中取数据,通过置换worker中Runnable对象,运行其run方法起到线程置换的效果,这样做的好处是避免多线程频繁线程切换,提高程序运行性能。

AQS

AbstractQueueSynchronizer,是阻塞式锁和相关的同步器工具的框架,定义了锁的实现机制,并开放出扩展的地方,让子类去实现。
用state属性表示资源的状态(独占或共享),子类需要定义如何维护这个状态,控制如何获取和释放锁。
提供了基于FIFO的等待队列+条件队列,等待队列类似于Monitor的EntryList,管理着获取不到锁的线程的排队和释放。条件队列是在一定场景下,对同步队列的补充,实现等待、唤醒机制,类似于Monitor的WaitSet。

标签:Java,synchronized,Thread,对象,并发,线程,run,执行
From: https://www.cnblogs.com/ting65536/p/17466348.html

相关文章

  • 我借助 AI 神器,快速学习《阿里的 Java 开发手册》,比量子力学还夸张
    我平时经常要看PDF,但是我看书贼慢,一个PDF差不多几十上百页,看一遍要花挺长时间。我记性还不好,看完之后,过些日子就记不清PDF是讲什么的了。为了找到PDF里的某些信息,又得再花时间。不过,现在这些问题都不是问题了。因为我最近发现了一个神器,1分钟就能读完一个PDF。上一次......
  • error:java: compilation failed: internal java compiler error
    转自:https://xie.infoq.cn/article/537f575c166d556db9773002f java:Compilationfailed:internaljavacompilererror解决办法:1、查看项目的jdk(Ctrl+Alt+shift+S)File->ProjectStructure->ProjectSettings->Project2、查看工程的jdk(Ctrl+Alt+shift+S)File->Pr......
  • linux设置开机启动nginx、java
    linux设置开机启动nginx、java1、开机启动nginx我是用yum安装的nginx,nginx启动程序在/usr/sbin/nginx#修改/etc/rc.d/rc.local文件#添加/usr/sbin/nginx#添加后执行chmod+x/etc/rc.d/rc.local#如果重启后没有自启成功,查看/var/log/boot.log日志中是否有错误#!/bin/bash#......
  • 48基于java的学生课程成绩系统设计与实现
    本章节给大家带来一个基于java的学生课程成绩管理系统设计与实现,可适用于学生学生课程管理系统,学生成绩管理系统,教务课程管理系统,教务系统,成绩系统,课程系统,校园管理系统,校园课程管理系统,大学校园课程管理系统等等。项目背景学生成绩管理系统是学校日常信息管理的一个重要内容......
  • Java爬虫通用模板它来了
    Java爬虫在实际应用中有很多场景,例如:数据挖掘和分析、搜索引擎、电商平台、数据更新、监控与预测等行业都需要爬虫借入,那么在实际爬虫中需要注意什么?又该怎么样快速实现爬虫?下面的文章值得看一看。单线程java爬虫以下是一个基本的Java爬虫模板,使用Jsoup库进行HTML解析和网络请求:im......
  • Java 深入学习(27) —— 反射:运行时的类型信息
    1什么是反射反射(Reflection)是Java程序开发语言的特征之一,它允许运行中的Java程序获取类的信息,并且可以操作类或对象的内部属性。通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。反射的核心是JVM在运行时才动态加载类或调用方法/访问属性,它不需要......
  • java~如何使用无符号整型
    在Java中,没有直接支持无符号整数的数据类型。Java的基本数据类型(如int、long、short、byte)都是带符号的,即它们可以表示正数和负数。.net中每种整型都有对应的无符号类型,它不会把取值范围分成正负两个区间,只在正整数范围内取值然而,你可以使用Java中的较大数据类型(如long......
  • 【JAVA】SHA加密
    1、代码packagecn.jiami;importjava.security.MessageDigest;importjava.security.NoSuchAlgorithmException;importorg.apache.commons.codec.binary.Hex;publicclassSHAUtils{protectedstaticMessageDigestmessageDigest=null;publicstaticS......
  • Java爬虫通用模板它来了
    Java爬虫在实际应用中有很多场景,例如:数据挖掘和分析、搜索引擎、电商平台、数据更新、监控与预测等行业都需要爬虫借入,那么在实际爬虫中需要注意什么?又该怎么样快速实现爬虫?下面的文章值得看一看。单线程java爬虫以下是一个基本的Java爬虫模板,使用Jsoup库进行HTML解析和网络请......
  • windows查看java进程, 终止进程命令
    查看:tasklist| findstr "java"终止:taskkill/pid20388/f/f表示强制终止......