作者:pox21s
在Java中,线程部分是一个重点,本篇文章说的是关于线程并发编程。JUC就是java.util .concurrent工具包的简称。这是一个处理线程的工具包,从JDK 1.5开始出现。
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个基本单位。
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、java虚拟机栈、本地方法栈等),但是同属一个进程的线程之间可以共享这个进程的所有资源。(只是进程含有的,线程自己的不能互相共享)
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。
相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
简而言之,一个程序至少有一个进程,一个进程至少有一个线程
线程的划分尺度小于进程,使得多线程程序的并发性高
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
线程在执行过程中与进程还是有区别的。每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口。**但是线程不能够独立执行,**必须依存在应用程序中,由应用程序提供多个线程执行控制
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别
线程执行开销小,但不利于资源的管理和保护;
而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
运行(RUNNABLE)
Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该线程的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。
就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
就绪状态(RUNNABLE之READY)
就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
调用线程的start()方法,此线程进入就绪状态。
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,其他线程也将进入就绪状态。
join()方法:
当前线程调用其他线程的join方法,当前线程进行阻塞状态,等待被调用线程执行完毕以后,结束阻塞状态,等待CPU时间片分配,当前线程才能继续执行。
t.join()/t.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入TIME_WAITING状态,当前线程不释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程进入就绪状态。
当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
yield 即 “谦让”,也是 Thread 类的方法。它让掉当前线程 CPU 的时间片,使正在运行中的线程重新变成就绪状态,并重新竞争 CPU 的调度权。它可能会获取到,也有可能被其他线程获取到。
锁池里的线程拿到对象锁后,进入就绪状态。
所有等待获取锁的线程都会进入锁池,进入阻塞状态。
线程调度程序从可运行池中选择一个线程作为当前线程时,线程进入运行态。这也是线程进入运行状态的唯一的一种方式。
阻塞(BLOCKED)
表示线程阻塞于锁。
等待(WAITING)
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
超时等待(TIMED_WAITING)
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
sleep()方法就会使线程进入到超时等待状态,并且不会释放机锁。
终止(TERMINATED)
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
线程的start()方法只能被调用一次。
这6种状态定义在Thread类的State枚举中,可查看源码进行一一对应。
sleep() 方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。
因为sleep() 是static静态的方法(在synchronized前面加上static则说明锁的是这个类,单synchronized锁的是实例对象,sleep底层是通过本地方法实现),他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池进入休眠,同时释放对象的机锁,使得其他线程能够访问这个对象,可以通过notify,notifyAll方法来唤醒等待的线程
notify,notifyAll方法并不会让当前线程进入休眠还会唤醒其他线程
sleep会休眠但是会把锁握在手里,其他线程调用相同的对象,要等待锁,这时,锁还在sleep手中,其他线程则只有等待醒来并执行完方法后释放锁,才能执行方法。
但wait会休眠但是不会将锁握在手里,其他线程调用相同对象时可以直接执行方法,不用等待当前休眠的进程醒来。
并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。
与可以一起出发的并发(concurrent)相对的是不可以一起出发的顺序(sequential):
顺序:上一个开始执行的任务完成后,当前任务才能开始执行
并发:无论上一个开始执行的任务是否完成,当前任务都可以开始执行(也就是说,A B 顺序执行的话,A 一定会比 B 先完成,而并发执行则不一定。)
与可以一起执行的并行(parallel)相对的是不可以一起执行的串行(serial):
串行:有一个任务执行单元,从物理上就只能一个任务、一个任务地执行
并行:有多个任务执行单元,从物理上就可以多个任务一起执行(也就是说,在任意时间点上,串行执行时必然只有一个任务在执行,而并行则不一定。)
综上,并发与并行并不是互斥的概念,只是前者关注的是任务的抽象调度、后者关注的是任务的实际执行。而它们又是相关的,比如并行一定会允许并发。
一种监视器,俗称为锁,是一种同步机制,保证同一个时间,只能有一个线程访问被保护的数据或代码(块)等。
JVM的同步基于进入和退出,进入持有锁,退出释放锁,这个是使用管程对象来实现的。
synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。
用户线程:使用者自己创建出来的线程(new)
守护线程:守护线程 – 也称“服务线程”,在没有用户线程可服务时会自动离开,JVm创建,当JVM中没有用户线程时,JVM会自动关闭。
通过setDaemon(true)可以将用户设置线程为“守护线程”;
守护线程就是JVM中存在的线程,当没有用户线程可以服务的时候JVM就会结束守护线程。(JVM中只有守护线程时也会结束)。用户线程就是用户自己创建出来的线程,当有用户线程存在的时候,守护线程才能存在。
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
通过在类名上继承Thread并重写其中的run()方法来实现,在创建实例的时候调用xxx.start()方法来启动线程
实现Runnable接口并重写其中的run()方法,创建实例以后通过放入线程来创建线程
public class Main {
public static void main(String[] args){undefined
// 创建并启动线程,这里的myThread已经实现了Runnable接口并重写了run()方法
MyThread2 myThread=new MyThread2();
Thread thread=new Thread(myThread);
thread().start();
// 或者new Thread(new MyThread2()).start();
}
}
实现Callable接口并重写其中的call()方法。和实现Runnable接口不同的是,Callable有返回值,并且需要配合FutureTask来接收返回值,Thread并不能直接接收Callable接口的实现类,只能接收FutureTask。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//实现Callable接口
public class CallableTest {
public static void main(String[] args) {
//执行Callable 方式,需要FutureTask 实现实现,用于接收运算结果
FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
new Thread(futureTask).start();
//接收线程运算后的结果
try {
Integer sum = futureTask.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
}
线程池提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提升了响应速度。实现了线程复用。
Callable<Singleton4> callable = new Callable<Singleton4>() {
@Override
public Singleton4 call() throws Exception {
return Singleton4.getInstance();
}
};
// 创建一个线程池,并设定大小
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 通过future来接收线程启动后返回的结果
Future<Singleton4> f1 = threadPool.submit(callable);
Future<Singleton4> f2 = threadPool.submit(callable);
// 获取返回值
Singleton4 s6 = f1.get();
Singleton4 s7 = f1.get();
System.out.println(s6 == s7);
threadPool.shutdown();
}
创建资源类,编写属性和操作方法
在资源类设置操作方法
创建多个线程,调用资源类的操作方法
防止虚假唤醒,将在资源类操作方法的判断条件设置在while中
为防止虚假唤醒,不要使用if来进行线程状态的判定,而是通过while来进行判断,实时判断更新。
Java自带的关键字,它最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性(排它性)。在发生异常的时候会自动释放锁。
由JVM来控制锁的开和关,用户无法控制。
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"售票员1").start();
new Thread(() ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"售票员2").start();
new Thread(() ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"售票员3").start();
}
}
class Ticket{
private int num = 30;
public synchronized void sale(){
if (num > 0 ){
// 获取当前执行的线程的名字
System.out.println(Thread.currentThread().getName()+"售卖第"+(num--)+"票"+"还剩下"+num+"票");
}
}
}
Lock不是java中的关键字,通过实例化来获取,可以让用户自己手动的上锁和解锁,如果没有设定解锁方式,在发生异常时不能正常解锁,则会发生死锁现象。
import java.util.concurrent.locks.ReentrantLock;
public class SaleTicketByLockDemo {
public static void main(String[] args) {
SaleTicketByLock ticket = new SaleTicketByLock();
new Thread(() ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"售票员1").start();
new Thread(() ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"售票员2").start();
new Thread(() ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"售票员3").start();
}
}
class SaleTicketByLock{
// 实现Lock
ReentrantLock lock =new ReentrantLock();
private int num = 30;
// 这里使用try finally 的方式来确保无论是否发生异常最后都能正确的关闭锁,以确保不会发生死锁的方式
public synchronized void sale(){
lock.lock();
try {
if (num > 0 ){
System.out.println(Thread.currentThread().getName()+"售卖第"+(num--)+"票"+"还剩下"+num+"票");
}
} finally {
lock.unlock();
}
}
}
来源
lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现。
异常是否释放锁
synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
原因:synchronized底层会释放两次锁,第一次释放为正常释放,第二个为当出现异常时的释放
是否响应中断
lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
是否知道获取锁
Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
Lock可以提高多个线程进行读操作的效率
readwritelock就是实现读线程共享。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择
synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度
lock可以通过condition来达到精确唤醒
synchronized只能随机唤醒或者全部唤醒
保证数据的可见性,能够保证一定的有序性,不能保证原子性,禁止指令重排
锁的强度:synchronized > lock > volatile ,推荐解决问题从底用到高
package syn;
/**
* @Author PoX21s
* @Date: 2021/11/1 17:01
* @Version 1.0
*/
public class ThreadDemo1 {
public static void main(String[] args) {
share share = new share();
new Thread(() ->{
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程一").start();
new Thread(() ->{
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程二").start();
}
}
class share{
private int num = 0;
public synchronized void incr() throws InterruptedException {
if (num == 1){
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName()+"::"+num);
this.notifyAll();
}
public synchronized void decr() throws InterruptedException {
if (num == 0){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"::"+num);
this.notifyAll();
}
}
我们可以将上面的线程数增加,就会发现结果不会出现0 1 0 1 交替出现,这是因为增加了多个线程以后,可能会出现这种情况,线程一已经将值加1,然后出于等待状态,然后其他线程执行了减1 的操作,接着通知了所有的线程,这个时候,线程一则会继续执行下面的代码(因为wait()方法在哪里睡就在哪里醒的特性导致),这就会导致虚假唤醒的情况。
将判断条件中的if改为while,在醒来以后继续判断。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author PoX21s
* @Date: 2021/11/1 17:01
* @Version 1.0
*/
public class ThreadDemo2 {
public static void main(String[] args) {
share1 share = new share1();
new Thread(() ->{
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程一").start();
new Thread(() ->{
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程二").start();
new Thread(() ->{
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程三").start();
new Thread(() ->{
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程四").start();
}
}
class share1 {
private int num = 0;
ReentrantLock lock = new ReentrantLock();
// 通过lock中的condition实例来实现类似synchronized中的wait和notify
private Condition condition = lock.newCondition();
public void incr() throws InterruptedException {
lock.lock();
try {
while (num == 1){
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName()+"::"+num);
condition.signalAll();
} finally {
lock.unlock();
}
}
public void decr() throws InterruptedException {
lock.lock();
try {
while (num == 0){
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName()+"::"+num);
condition.signalAll();
} finally {
lock.unlock();
}
}
}
按照规定的方式执行,执行完以后,更改标志位,唤醒下一个线程。使用Lock实现。
package syn;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author PoX21s
* @Date: 2021/11/1 17:01
* @Version 1.0
*/
public class ThreadDemo3 {
public static void main(String[] args) {
share2 share2 = new share2();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share2.print5();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"aa").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share2.print10();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"bb").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
share2.print15();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"cc").start();
}
}
class share2 {
// 设置标志位
private int flag = 1;
private ReentrantLock lock = new ReentrantLock();
// 一个Lock对象中可以创建多个Condition实例(即对象监视器)
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print5() throws InterruptedException {
lock.lock();
try {
while (flag != 1){
c1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+"打印第"+i);
}
flag = 2;
c2.signal();
} finally {
lock.unlock();
}
}
public void print10() throws InterruptedException {
lock.lock();
try {
while (flag != 2){
c2.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+"打印第"+i);
}
flag = 3;
c3.signal();
} finally {
lock.unlock();
}
}
public void print15() throws InterruptedException {
lock.lock();
try {
while (flag != 3){
c3.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+"打印第"+i);
}
flag = 1;
c1.signal();
} finally {
lock.unlock();
}
}
}
这里通过当一个线程运行以后,更改判断条件,并通知下一个线程来执行达到按规则执行的目的。
在多个线程同时读取和写入(并发修改)的时候可能会出现线程并发安全问题,下面通过三种方式来解决,推荐使用第三种。
Exception in thread "24" java.util.ConcurrentModificationException
本质是在Vector的添加方法加入了synchronized关键字,来实现线程并发安全。已过时,不推荐。
public class ThreadDemo4 {
public static void main(String[] args) {
// List list = new ArrayList<>();
// 通过改变创建list引用的对象来实现在添加时的线程安全
List<String> list = new Vector<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,7));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
本质也是在add方法中增加了synchronized关键字。
public class ThreadDemo4 {
public static void main(String[] args) {
// 本质通过在add方法上添加了线程同步关键字实现
List<String> list = Collections.synchronizedList(new ArrayList<String>());
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,7));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
写时复制,本质是在写入的时候线程将原本的数组进行一个复制,在复制上的数组进行写入操作,然后合并更新原数组,后面的线程就使用新的数组进行写入。
在源代码中也是通过lock来实现的。
其他集合也有相应的类,如:new copyonwritearrayset()等。推荐使用。
public class ThreadDemo4 {
public static void main(String[] args) {
// 写时复制技术,先复制出来一份,当前写完以后合并更新到原数组,后面的线程使用新的数组进行操作
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,7));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
通过synchronized关键字实现。
public class ThreadDemo5 {
public static void main(String[] args) {
// HashSet set = new HashSet<>();
Set<String> set;
set = Collections.synchronizedSet(new HashSet<String>());
for (int i = 0; i < 30; i++) {
new Thread(() -> {
set.add(UUID.randomUUID().toString().substring(0,6));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
添加synchronized关键字代码块,实现线程同步。推荐使用,性能和HashMap差距不大。
public class ThreadDemo5 {
public static void main(String[] args) {
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 30; i++) {
String key = String.valueOf(i);
new Thread(() -> {
map.put(key,UUID.randomUUID().toString().substring(0,6));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
方法上的synchronized锁,单synchronized的时候锁的是当前实例对象(this),其他实例对象不受影响
方法上的synchronized锁,含有static时,锁的是当前的class类,所有实例对象都受影响
但是上面的两种情况只能限制含有synchronized关键字的方法,如果是一个普通方法则不受限制(不含有synchronized,没有锁,那么就没有进出限制)
当在一个类中含有static的synchronized方法和不含有static的synchronized时,当两个方法都被调用了,但是第一个锁的是整个类,但是类中的方法没有被锁住,也就是这是两把锁,虽然大楼的门关了,但是大楼的房间还是可以随便进入的,其他方法也可以执行
class Person{
public synchronized void test(){
// ....
}
public static synchronized void test2(){
// ....
}
public void test3(){
// ....
}
}
// 当只有单synchronized时锁的是p1实例对象
// 当在synchronized前面加了时,锁的是Person类
public class Test{
// psvm
Person p1 = new Person();
}
使用非公平锁,当前线程发现有空就会去使用,不会考虑其他线程
如果占用失败,则会采用类似公平锁的方式,到队列后面
效率高,但可能会造成单个线程做完了所有事,其他线程没事可干,一直陪跑
synchronized是非公平锁
lock默认是非公平,但是可以设置为公平锁 new reentrant(true)
使用公平锁,当前线程发现有空的时候,会询问其他线程是否有线程要使用,如果没有其他线程要使用,就自己用,如果有其他线程用,就排队。
按照申请锁的顺序来执行
效率相对于非公平锁会低,因为会有询问的过程,但是相对非公平锁来说,每一个线程都有执行的机会,不会出现单线程干完所有事的情况(线程饥饿问题)
synchronized(隐式的上锁和解锁交给JVM)和Lock(显式的上锁和解锁交给用户本身)都是可重入锁
可重入锁又称为递归锁,当一把锁内部还有其他锁的时候,此时线程如果获得最外层的锁,那么内部的锁线程也可以随意进入,下面通过synchronized来演示
最大的作用就是避免死锁,还是要注意lock的锁释放
public class ThreadDemo6 {
public static void main(String[] args) {
Object o = new Object();
new Thread(() -> {
synchronized (o){
System.out.println(Thread.currentThread().getName()+"外部的锁");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+"中间的锁");
synchronized (o){
System.out.println(Thread.currentThread().getName()+"内部的锁");
}
}
}
},"线程一").start();
}
}
对于线程本身当获得了外部的锁以后是可以随意的进入执行方法内部的锁,但是还是要及时进行解锁操作,虽然当前线程不会收到没有解锁的影响,但是如果上来锁但一直没有解锁的话,其他的线程就无法获得执行的机会,对于synchronized来说是自动上锁和解锁,所以没有解锁的问题,但对于lock来说是用户自己进行上锁和解锁操作,所以虽然获得了外部的锁,内部的锁对当前线程本身是没有影响的,但为了其他线程能够获得执行的机会,使用lock的时候也要及时的进行解锁操作。
两个或两个以上的线程在执行过程中,为了争夺资源而造成一种相互等待的情况,如果没有外力干涉,则相互都无法继续往下执行。
线程A现在拥有锁A,线程B现在拥有锁B,此时,线程A试图获取锁B,线程B试图获取锁A,因为双方都没有进行释放锁,所以会导致互相一直等待,造成死锁现象。
public class DeadLock {
static Object o1 = new Object();
static Object o2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (o1){
System.out.println(Thread.currentThread().getName()+"持有o1锁,试图获取o2锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println(Thread.currentThread().getName()+"试图获取o2锁");
}
}
},"线程一").start();
new Thread(() -> {
synchronized (o2){
System.out.println(Thread.currentThread().getName()+"持有o2锁,试图获取o1锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println(Thread.currentThread().getName()+"试图获取o2锁");
}
}
},"线程二").start();
}
}
// 输出结果
// 线程一持有o1锁,试图获取o2锁
// 线程二持有o2锁,试图获取o1锁
// (一直等待中)
可以通过java中自带的工具查询:jps和jstack
PS F:\Java\JUC> jps -l
17044 sun.tools.jps.Jps
18692 org.jetbrains.jps.cmdline.Launcher
13612
21276 syn.DeadLock
PS F:\Java\JUC> jstack 21276
Found 1 deadlock. #返回当前为死锁状态
在jdk1.5以后新增的一种创建线程的方式,和Runnable的区别
不能直接传递给Thread,Thread只能接收Runnable对象,因此为了让Thread能够接收Callable对象,要通过中间类来实现,这就是FutureTask类。
在不影响主线程的情况下,可以再开启其他线程来执行其他操作,这些线程执行完了以后,把结果返回给主线程,并且所有结果只会汇总一次
class Demo implements Callable{
@Override
public Object call() throws Exception {
return 1+2+3;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Demo c1 = new Demo();
FutureTask<Integer> task1 = new FutureTask<Integer>(c1);
FutureTask<Integer> task2 = new FutureTask<Integer>(() -> {
return 1024;
});
new Thread(task1,"线程一").start();
new Thread(task2,"线程二").start();
// 等待得到返回结果
while (!task1.isDone()){
System.out.println("等待返回结果");
}
System.out.println("结果:");
// 当得到一次结果以后,以后的get()则会直接调取结果,不用再进行运算
// 即只用汇总一次
System.out.println(task1.get());
System.out.println(task1.get());
// 当所有的新开起的线程都结束了,主线程才会结束
System.out.println(task2.get());
System.out.println(Thread.currentThread().getName()+"over");
}
}
可以设定初始值,当这个类中的count属性不为0的时候,会让其他线程进入阻塞状态,调用await()方法,只有减为0才会唤醒其他线程往下执行,也就是解除await()方法只能是count减为0的时候
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch count = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+ " 号同学离开教室");
count.countDown();
},String.valueOf(i)).start();
}
count.await();
System.out.println(Thread.currentThread().getName()+" 关闭教室门");
}
}
设定一组现成的额个数,多个线程开始执行,当开始执行的线程达到设定的一组的个数时,这一组的线程同时开始执行。没有达到数量时,已经开始执行的线程处于阻塞状态,等待其他线程的到来,然后再一起执行。
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(6, new Runnable() {
@Override
public void run() {
System.out.println("已经集齐了7颗龙珠,召唤神龙");
}
});
for (int i = 0; i < 7; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName()+ " 号龙珠被得到");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
设定许可的数量,当一个线程获取到了许可,则可以执行方法,其他开启的线程没有获得许可,则进入等待状态,当有线程释放许可以后,等待的线程争夺许可,获得了许可执行,没有获得的继续等待。
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+" 号获取到车位");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+" 离开车位====》");
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
每次进行操作都会进行锁上,操作完以后才会解锁,当一个线程在执行的时候,其他线程都处于等待阻塞状态
例如:synchronized关键字
线程在执行的时候不会将要处理的资源锁上,会将资源复制一份,然后再复制的资源上进行操作,操作完以后提交到原有的数据上进行更新操作。但是在提交的时候,要进行比对判断,判断复制的原内容和现在的情况是否相同 ==》例如:现在有两个线程要操作同一个资源,它们在操作资源的时候都会先进行复制,然后在复制的资源上进行操作,但是现在线程一先处理完,提交更新了,这时的原资源已经发生了改变,当其他线程提交的时候,发现自己最开始复制的内容,和现在的资源内容不相同,则会提交失败,而是继续复制当前的内容,然后再提交判断。
例如:CAS,通过自旋来抵消阻塞,并不会真正的加锁而是通过代码编程的方式来解决线程同步问题。
在线程处理资源的时候,会将整个数据表都锁上,不允许其他线程进行操作,不会发生死锁现象。
在线程处理资源时,只会将要处理的一行进行上锁,表中的其他行是允许其他线程进行操作的,会出现死锁现象,即两个或两个以上的线程互相等待对方释放锁,而导致的多线程间互相等待的情况。
在读的时候,允许多个线程一起读,共享锁,会发生死锁现象 ==》 例如:多个线程都在读取同一行资源,但是互相之间都想要修改,则都在互相等待对方读取完毕然后进行修改的操作,因为都想修改,则会导致互相等待的情况。
在写的时候,不允许多个线程共同操作,独享锁,但也会发生死锁现象 == 》 例如:多个线程都在修改不同的资源,但是都又想操作对方的资源,而出现互相等待死锁的情况。
读的时候可以共享,但是写的时候只能有一个线程进行操作。但是不能同时存在读写操作,读写是互斥的,读读是共享的。
比其synchronized和lock独占锁的情况,可以提升读的性能
public class ReadWriteLockDemo {
public static void main(String[] args) {
ContextDemo context = new ContextDemo();
for (int i = 0; i < 5; i++) {
final int num = i;
new Thread(() -> {
try {
context.put(num+"",num+"");
} catch (InterruptedException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
final int num = i;
new Thread(() -> {
try {
context.get(num+"");
} catch (InterruptedException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
class ContextDemo {
private volatile Map<String,Object> map = new HashMap<>();
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void put(String key, Object o) throws InterruptedException {
lock.writeLock().lock();
System.out.println(Thread.currentThread().getName()+" 正在写入~~");
TimeUnit.MICROSECONDS.sleep(300);
map.put(key,o);
System.out.println(Thread.currentThread().getName()+" 写入完毕~~");
lock.writeLock().unlock();
}
public Object get(String key) throws InterruptedException {
Object res = null;
lock.readLock().lock();
System.out.println(Thread.currentThread().getName()+" 正在读取~~");
TimeUnit.MICROSECONDS.sleep(300);
res = map.get(key);
System.out.println(Thread.currentThread().getName()+" 读取完毕~~");
lock.readLock().unlock();
return res;
}
}
// 结果
1 正在写入~~
1 写入完毕~~
2 正在写入~~
2 写入完毕~~
3 正在写入~~
3 写入完毕~~
0 正在写入~~
0 写入完毕~~
4 正在写入~~
4 写入完毕~~
// 写是独享锁,读是共享锁,一起读
0 正在读取~~
1 正在读取~~
2 正在读取~~
4 正在读取~~
3 正在读取~~
1 读取完毕~~
0 读取完毕~~
3 读取完毕~~
2 读取完毕~~
4 读取完毕~~
写锁可以降级为读锁,但是读锁不能升级写锁,只有在读锁释放了以后才能再进行获取写锁的操作,然后进行写的操作。写的时候可以进行读,但是读的时候不能进行写
阻塞队列是一个共享队列,一边进行往队列中进行添加元素,一边进行往队列进行取出元素。
对于添加元素的线程来说,当阻塞队列中的空间为满的时候,它则不能再继续添加元素,这时线程的操作就会被阻塞,线程也就被阻塞了。
同理对于队列另一边的线程来说,当阻塞队列中的空间为空的时候,它则不能继续进行取出元素,这时线程的操作就会被阻塞,线程也就被阻塞了。
当有线程在阻塞状态,等待阻塞队列空间发生变化,当空间发生变化更新,阻塞队列会唤醒相应的线程。阻塞队列的优点就是,阻塞和唤醒线程,都进行了自动化,无需外界干扰。
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
// 下面的演示方法都为一个共享队列
BlockingQueue blockingQueue = new ArrayBlockingQueue(3);
// 第一组方法演示,这组会主动抛出异常
// System.out.println(blockingQueue.add("aa"));
// System.out.println(blockingQueue.add("aa"));
// System.out.println(blockingQueue.add("aa"));
// System.out.println(blockingQueue.add("aa"));
// 移除当前第一个元素
// System.out.println(blockingQueue.remove());
// blockingQueue.remove();
// blockingQueue.remove();
// blockingQueue.remove();
// 检查当前队列,并查看队列头部 System.out.println(blockingQueue.element());
/*Exception in thread "main" java.lang.IllegalStateException: Queue full
at java.util.AbstractQueue.add(AbstractQueue.java:98)
at java.util.concurrent.ArrayBlockingQueue.add(ArrayBlockingQueue.java:312)
at BlockingQueueDemo.main(BlockingQueueDemo.java:18)*/
// System.out.println("===============================");
// 第二组方法演示,会有返回时候加入成功
// System.out.println(blockingQueue.offer("bbb"));
// System.out.println(blockingQueue.offer("ccc"));
// System.out.println(blockingQueue.offer("ddd"));
/* true
false
false*/
// 取出当前队列中的值,如果取出的时候队列为空,则会返回null
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// 查看当前队列的第一个值,但是不取出
// System.out.println(blockingQueue.peek());
// System.out.println(blockingQueue.peek());
// System.out.println(blockingQueue.peek());
System.out.println("===============================");
// 演示第三组方法,满足相应的空间条件可以执行,否则线程进入阻塞等待状态
// 添加元素,如果为空则可以添加成功,如果满则无法进行添加,线程阻塞
// blockingQueue.put("tt1");
// blockingQueue.put("tt2");
// blockingQueue.put("tt3");
// 同理
// blockingQueue.take();
// blockingQueue.take();
// blockingQueue.take();
// 演示第四组方法,返回添加情况,设置超时等待,超过时间则退出
System.out.println(blockingQueue.offer("aa", 3, TimeUnit.SECONDS));
}
}
public class SynchronousQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> bq = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName()+ "放入第一个元素");
bq.put("1");
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+ "放入第二个元素");
bq.put("2");
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+ "放入第三个元素");
bq.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程一").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+" 等待两秒后取出元素");
TimeUnit.SECONDS.sleep(2);
bq.take();
System.out.println(Thread.currentThread().getName()+" 已取出第一个元素");
System.out.println(Thread.currentThread().getName()+" 等待两秒后取出第二个元素");
TimeUnit.SECONDS.sleep(2);
bq.take();
System.out.println(Thread.currentThread().getName()+" 已取出第二个元素");
System.out.println(Thread.currentThread().getName()+" 等待两秒后取出第三个元素");
TimeUnit.SECONDS.sleep(2);
bq.take();
System.out.println(Thread.currentThread().getName()+" 已取出第三个元素");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程二").start();
}
}
只有当前队列中的元素取出,才能放入下一个元素,否则就进入阻塞状态
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
线程在线程池中提前创建,当有任务来的时候,线程池可以调用线程来处理,提高了响应的速度,可以方便的管理线程,也可以减少内存的消耗。提高线程的复用性。
Java中通过Executor工具类来实现线程池的创建和管理
public class ThreadPoolDemo {
public static void main(String[] args) {
// 第一种通过Executors.newFixedThreadPool来创建大小并设定线程数量
ExecutorService threadPool1 = Executors.newFixedThreadPool(3);
// 第二种通过Executors.newSingleThreadExecutor创建拥有一个线程的线程池
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
// 第三种通过Executors.newCachedThreadPool创建一个自适应的线程池大小
ExecutorService threadPool3 = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 20; i++) {
threadPool3.execute(() -> {
try {
System.out.println(Thread.currentThread().getName()+ " 在办理业务");
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+ " 业务办理完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} finally {
// 处理完以后一定要关闭线程池
threadPool3.shutdown();
}
}
}
通过源码分析这三种创建方式,都是通过ThreadPoolExecutor来实现的,现在我们来分析一下这个类
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
int corePoolSize
常驻的线程数量,也称为核心的线程数量
int maximumPoolSize
线程池中最大的线程数量
long keepAliveTime
这个和下面的参数组合使用,上面这个是除了核心线程以外的存活时间,超过了存活时间则关闭,下面的参数是设定时间的类型
TimeUnit unit
BlockingQueue workQueue
阻塞队列,当目前线程池中的线程都在进行任务处理的时候,将新来的任务,添加到阻塞队列中,等待有线程来处理任务
ThreadFactory threadFactory
线程工厂,负责创建线程
RejectedExecutionHandler handle
拒绝策略。当已经达到线程池的最大线程数以后,再来的任务,无法执行,而执行的一种拒绝策略
当有任务来的时候才会创建线程池
先用常驻线程来进行任务处理,当常驻线程都在进行任务处理,再到来的任务进入阻塞队列
当阻塞队列已经排满了,这时启用线程池中除常用线程以外的线程(线程池最大线程数 - 常驻线程)处理到来的任务
在阻塞队列满以后到达的任务会直接用备用的线程处理,而不用等待。阻塞队列等待的任务继续等待。
如果线程池中的已经达到最大线程数量在工作,继续有任务到来,则执行拒绝策略
一般不使用上面的方式创建线程池,而是自定义。
使用上面的方式,可能会造成任务堆积或资源浪费。
public class NewThreadByDIY {
public static void main(String[] args) {
ExecutorService diyThread = new ThreadPoolExecutor(
2,
7,
3L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
try {
for (int i = 0; i < 20; i++) {
diyThread.execute(() -> {
try {
System.out.println(Thread.currentThread().getName()+ " 在办理业务");
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+ " 业务办理完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} finally {
// 处理完以后一定要关闭线程池
diyThread.shutdown();
}
}
}
将大问题化为小问题,然后合并
public class Task {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建对象
MyTask myTask = new MyTask(1, 100);
// 创建分支合并对象池
ForkJoinPool pool = new ForkJoinPool();
// 执行
ForkJoinTask<Integer> submit = pool.submit(myTask);
// 获取结果
Integer integer = submit.get();
System.out.println(integer);
// 关闭线程池
pool.shutdown();
}
}
class MyTask extends RecursiveTask<Integer> {
private static final int VALUE = 10;
private int begin;
private int end;
private int res;
public MyTask(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
protected Integer compute() {
if ((end - begin) <= VALUE){
for (int i = begin; i <= end; i++) {
res = res + i;
}
} else {
int mid = (begin + end) / 2;
// 拆分左边
MyTask task1 = new MyTask(begin, mid);
// 拆分右边
MyTask task2 = new MyTask(mid+1, end);
task1.fork();
task2.fork();
// 合并
res = task1.join() + task2.join();
}
return res;
}
}
我们常用的一些请求都是同步回调的,同步回调是阻塞的,单个的线程需要等待结果的返回才能继续往下执行。
有的时候,我们不希望程序在某个执行方法上一直阻塞,需要先执行后续的方法,那就是这里的异步回调。我们在调用一个方法时,如果执行时间比较长,我们可以传入一个回调的方法,当方法执行完时,让被调用者执行给定的回调方法。
异步回调,不需要等待分派的任务执行完毕以后再进行下面的任务执行,而是继续往下面执行,当分派的任务执行完毕以后接收结果即可。
CompletableFuture.runAsync
没有返回值
CompletableFuture.supplyAsync
有返回值
public class ThreadDemo7 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 没有返回值
CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName() + "CompletableFuture1");
});
runAsync.get();
// 有返回值
CompletableFuture<Object> supplyAsync = CompletableFuture.supplyAsync(() -> {
return 77;
});
supplyAsync.whenComplete((u,t) -> {
// t打印返回值
// u打印异常信息
System.out.println(Thread.currentThread().getName() + t);
System.out.println(Thread.currentThread().getName() + u);
}).get();
}
}