深入理解Java多线程(各种方法和生命周期)和各种锁(自旋锁、轻量级锁、重量级锁、读写锁、可重入锁、公平锁、可中断锁等)

上一篇博客写了Java的内存模型和Java多线程底层的一些东西,但是总是感觉有点理论化,平时用线程方面的知识根本就没有用过一样。所以这一篇来总结一下我们用到的多线程知识。

首先创建线程:

俩种方式

1.继承Thread类重写run() 方法 

public class FirstThread extends Thread{

	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			
			System.out.println("这是"+this.getName()+"线程");
		}
	}

	
	
}

2.实现Runnable接口重写run() 方法 

public class SecondThread implements Runnable{

	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println("这是secondThread线程");
		}
		
	}

}

虽然是俩种创建方式,但是Thread其实也是实现了Runnable接口的

class Thread implements Runnable 

而我们都知道要想使线程处于就绪状态,就得调用他的start()方法,而Runnable中只有一个方法那就是run方法,所以要用Runnable来创建线程后还得再用Thread类来转化一下。如下:

public static void main(String[] args) throws ClassNotFoundException, IOException, InterruptedException {
		SecondThread st=new SecondThread();
		FirstThread ft=new FirstThread();
		Thread td=new Thread(st);
		td.start();
		ft.start();
	}
	

这是创建线程,那么线程的生命周期是什么呢?

主要有5种生命状态:1.线程的创建 2.就绪状态 3.运行状态  4.阻塞状态  5.终止状态

当我们使用new创建了一个线程实例时,这是线程处于创建状态,而当调用了start方法之后线程会正式进入就绪状态,当此线程抢到了cpu之后就会进入到运行状态,然后要是顺利的话,就可以一直到运行结束,进入死亡状态,要是不顺利的话比如调用了wait()方法就会出现阻塞状态(这之后会详细介绍)。

接下来就是线程的几个常用方法了。在此之前先运行一下之前的俩个线程。

这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程

public static native void yield();

public static native Thread currentThread();

public static native void sleep(long millis) throws InterruptedException;

public final synchronized void join(long millis)

public final native boolean isAlive();

public final synchronized void setName(String name)

 public final String getName() 

public final int getPriority()

public final void setPriority(int newPriority) 

接下来一个一个演示这些方法的作用:

1.yield()此方法的作用是让当前线程放弃CPU的使用权,转而变成就绪状态再次和其他线程抢夺cpu资源(有些文章上面会说此方法会让出cpu给其他同等级的线程使用,然后此线程便不再参与争夺,我经过验证觉得这种说法不太严谨,贴出我的代码)

public class YieldTest implements Runnable{

	private String name;
	public YieldTest(String name) {
		this.name=name;
	}
	
	
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName()+":"+i);
			if(i==0) {
				Thread.yield();
			}
		}
	}
//上面是Runnable类中的东西
	public static void main(String[] args) throws ClassNotFoundException, IOException, InterruptedException {
		Thread t1=new Thread(new YieldTest("t1"));
		Thread t2=new Thread(new YieldTest("t2"));
		t1.start();
		t2.start();
	}
结果为
Thread-1:0
Thread-0:0
Thread-1:1
Thread-0:1
Thread-1:2
Thread-1:3
Thread-1:4
Thread-0:2
Thread-1:5
Thread-0:3
Thread-1:6
Thread-1:7
Thread-1:8
Thread-1:9
Thread-1:10
Thread-1:11
Thread-1:12
Thread-1:13
Thread-1:14
Thread-1:15
Thread-1:16
Thread-1:17
Thread-1:18
Thread-1:19
Thread-1:20
Thread-1:21
Thread-1:22
Thread-1:23
Thread-1:24
Thread-1:25
Thread-1:26
Thread-1:27
Thread-1:28
Thread-1:29
Thread-1:30
Thread-1:31
Thread-1:32
Thread-1:33
Thread-1:34
Thread-1:35
Thread-1:36
Thread-1:37
Thread-1:38
Thread-1:39
Thread-1:40
Thread-1:41
Thread-1:42
Thread-1:43
Thread-1:44
Thread-1:45
Thread-1:46
Thread-1:47
Thread-1:48
Thread-1:49
Thread-1:50
Thread-1:51
Thread-1:52
Thread-1:53
Thread-0:4
Thread-1:54
Thread-0:5
Thread-1:55
Thread-0:6
Thread-0:7
Thread-0:8
Thread-0:9
Thread-0:10
Thread-0:11
Thread-0:12
Thread-0:13
Thread-0:14
Thread-0:15
Thread-0:16
Thread-0:17
Thread-0:18
Thread-0:19
Thread-0:20
Thread-0:21
Thread-0:22
Thread-0:23
Thread-0:24
Thread-0:25
Thread-0:26
Thread-0:27
Thread-0:28
Thread-0:29
Thread-0:30
Thread-0:31
Thread-0:32
Thread-0:33
Thread-0:34
Thread-0:35
Thread-0:36
Thread-0:37
Thread-0:38
Thread-0:39
Thread-0:40
Thread-0:41
Thread-0:42
Thread-0:43
Thread-0:44
Thread-0:45
Thread-0:46
Thread-0:47
Thread-0:48
Thread-0:49
Thread-0:50
Thread-0:51
Thread-0:52
Thread-0:53
Thread-0:54
Thread-0:55
Thread-0:56
Thread-0:57
Thread-0:58
Thread-0:59
Thread-0:60
Thread-0:61
Thread-0:62
Thread-0:63
Thread-0:64
Thread-0:65
Thread-0:66
Thread-0:67
Thread-0:68
Thread-0:69
Thread-0:70
Thread-0:71
Thread-0:72
Thread-0:73
Thread-0:74
Thread-0:75
Thread-0:76
Thread-0:77
Thread-0:78
Thread-0:79
Thread-0:80
Thread-0:81
Thread-0:82
Thread-0:83
Thread-0:84
Thread-0:85
Thread-0:86
Thread-0:87
Thread-0:88
Thread-0:89
Thread-0:90
Thread-0:91
Thread-0:92
Thread-0:93
Thread-0:94
Thread-0:95
Thread-0:96
Thread-0:97
Thread-0:98
Thread-0:99
Thread-1:56
Thread-1:57
Thread-1:58
Thread-1:59
Thread-1:60
Thread-1:61
Thread-1:62
Thread-1:63
Thread-1:64
Thread-1:65
Thread-1:66
Thread-1:67
Thread-1:68
Thread-1:69
Thread-1:70
Thread-1:71
Thread-1:72
Thread-1:73
Thread-1:74
Thread-1:75
Thread-1:76
Thread-1:77
Thread-1:78
Thread-1:79
Thread-1:80
Thread-1:81
Thread-1:82
Thread-1:83
Thread-1:84
Thread-1:85
Thread-1:86
Thread-1:87
Thread-1:88
Thread-1:89
Thread-1:90
Thread-1:91
Thread-1:92
Thread-1:93
Thread-1:94
Thread-1:95
Thread-1:96
Thread-1:97
Thread-1:98
Thread-1:99

原谅我一下给贴出来了,但是是为了证明我的验证的正确性,使用yield方法会使当前正在运行的线程让出cpu,转而进入就绪状态,会再次与其他线程争夺cpu,所以它完全有可能会再次执行。

2.currentThread()

此方法是Thread的静态方法,作用是返回当前正在使用cpu资源的线程,验证代码如下:

public class YieldTest implements Runnable{

	private String name;
	public YieldTest(String name) {
		this.name=name;
	}
	
	
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println("当前线程的引用:"+Thread.currentThread()+"-----"+Thread.currentThread().getName()+":"+i);
			
			if(i==0) {
				Thread.yield();
			}
		}
	}

	
	
}
主方法为:
public static void main(String[] args) throws ClassNotFoundException, IOException, InterruptedException {
		Thread t1=new Thread(new YieldTest("t1"));
		Thread t2=new Thread(new YieldTest("t2"));
		t1.start();
		t2.start();
	}
运行结果:
当前线程的引用:Thread[Thread-0,5,main]-----Thread-0:0
当前线程的引用:Thread[Thread-0,5,main]-----Thread-0:1
当前线程的引用:Thread[Thread-0,5,main]-----Thread-0:2
当前线程的引用:Thread[Thread-0,5,main]-----Thread-0:3
当前线程的引用:Thread[Thread-0,5,main]-----Thread-0:4
当前线程的引用:Thread[Thread-0,5,main]-----Thread-0:5
当前线程的引用:Thread[Thread-0,5,main]-----Thread-0:6
当前线程的引用:Thread[Thread-0,5,main]-----Thread-0:7
当前线程的引用:Thread[Thread-0,5,main]-----Thread-0:8
当前线程的引用:Thread[Thread-0,5,main]-----Thread-0:9
当前线程的引用:Thread[Thread-1,5,main]-----Thread-1:0
当前线程的引用:Thread[Thread-1,5,main]-----Thread-1:1
当前线程的引用:Thread[Thread-1,5,main]-----Thread-1:2
当前线程的引用:Thread[Thread-1,5,main]-----Thread-1:3
当前线程的引用:Thread[Thread-1,5,main]-----Thread-1:4
当前线程的引用:Thread[Thread-1,5,main]-----Thread-1:5
当前线程的引用:Thread[Thread-1,5,main]-----Thread-1:6
当前线程的引用:Thread[Thread-1,5,main]-----Thread-1:7
当前线程的引用:Thread[Thread-1,5,main]-----Thread-1:8
当前线程的引用:Thread[Thread-1,5,main]-----Thread-1:9

3.sleep(long millis):此方法是让线程进入休眠状态,但是不放弃对象锁(与wait的区别),注意:参数的单位是毫秒,到达时间之后会自动进入就绪状态。

public class SecondThread implements Runnable{

	@Override
	public void run() {
		try {
			Thread.sleep(10000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		for (int i = 0; i < 10; i++) {
			System.out.println("这是secondThread线程");
		}
		
	}

}

public class FirstThread extends Thread{

	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			
			System.out.println("这是"+this.getName()+"线程");
		}
	}

	
	
}

public static void main(String[] args) throws ClassNotFoundException, IOException, InterruptedException {
		FirstThread ft=new FirstThread();
		SecondThread st=new SecondThread();
		Thread st1=new Thread(st);
		st1.start();
		ft.start();
	}

这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程

4.join():此方法比较难理解一点结合代码看。

public static void main(String[] args) throws ClassNotFoundException, IOException, InterruptedException {
		FirstThread ft=new FirstThread();
		SecondThread st=new SecondThread();
		Thread st1=new Thread(st);
		st1.start();
		st1.join();
		ft.start();
	}
	
//这里只贴出主方法,因为俩个线程类在上面贴过了。
结果为:
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是secondThread线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程
这是Thread-0线程

其实Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。这个串行并不是我们使用锁那种理解,如上当在主方法中执行st1的join方法时,那么主方法将会等着st1中执行完毕才会继续向下执行,也就是执行ft这个线程。

它还有一个重载的方法就是join(long millis)单位是毫秒

此方法和join()基本一致,就是主线程会等st1milis时间后,他会再次和st1争夺cpu,当然此方法使用时要注意的是不能放在start()方法前面,否则无效。

剩下的几个方法

public final native boolean isAlive();   //判断线程是否活着

public final synchronized void setName(String name)//设置线程的名字

 public final String getName() //获取线程的名字

public final int getPriority()//获取线程的优先级

public final void setPriority(int newPriority) //设置线程的优先级

关于线程的优先级这里就不详细说了,因为Java跨平台的原因,所以优先级不一定有效,而且我们平时也很少使用。

就下来就是线程同步的问题。

先看和线程同步有关的方法有哪些?

1.sleep()

2.yield()

3.wait()   //这个方法是Object类中的方法,使用它时会使当前正在使用cpu资源的线程堵塞,并且它堵塞线程之后,线程不会自己返回到就绪状态,而是等待notify或notifyAll方法将其返回到就绪状态。此方法释放对象锁。

4.notify()  //此方法用来释放wait方法引起的堵塞现象,只能解放一个由wait引起堵塞的线程。

5.notifyAll()  //此方法可以解放所有由wait引起堵塞的线程。

以上三个方法会在下面生产者和消费者例子中使用,这里不详细说。

了解了线程同步方法以及同步原理之后,我们可以来看看同步的实例:

public class Produce implements Runnable{

	private Apple apple;
	public Produce(Apple apple) {
		this.apple=apple;
	}
	
	@Override
	public void run() {
		for(int i=0;i<5;i++) {
			try {
				apple.increace();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
	}

	
	
}
//此为生产者
public class Customer implements Runnable{

	private Apple apple;
	public Customer(Apple apple) {
		this.apple=apple;
	}
	
	@Override
	public void run() {
		
		for(int i=0;i<5;i++) {
			apple.recreate();;
		}
		
	}
	
}
//此为消费者
public class Apple{

	private int apple=0;
	
	
	public synchronized void increace() throws InterruptedException {
		while(apple==5) {
			try {
				notify();//先换醒其他线程,在关掉当前线程	
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
		}
		apple++;
		System.out.println("生产苹果成功");
		
	}
	public void recreate() {
		while(apple==0) {
			try {
				notify();
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
		}
		apple--;
		System.out.println("消费成功");
		
	}
}
//此为他们的共享变量
生产苹果成功
生产苹果成功
生产苹果成功
生产苹果成功
生产苹果成功
消费成功
消费成功
消费成功
消费成功
消费成功

此为结果

注意wait(),notify(),notifyAll()这些方法只能在synchronized关键字里面使用。

生产者和消费者是一个最典型的的不同线程之间进行变量共享,并且保持数据同步的一个例子。

到这里线程的生命周期和线程的几个方法以及和线程同步的几个方法就已经讲完了。

接下来就是几个关于锁机制的几个关键字了。

来看这俩个synchronized和volatile,这俩个关键字都是可以做到变量共享的,synchronized这个大家应该非常熟悉,而volatile这个关键字的话在我的上一篇博客里面有详细介绍,所以有不清晰的可以去那里看一看。

那么Java的线程锁呢,又有哪些呢?

java包含两种锁机制:synchronized和java.util.concurrent.Lock。

此为多线程内置的关键字,而Lock则是一个接口。

接下来先介绍几个锁概念

1.可重入锁:如果A方法中调用了B方法,如果A方法获取了该类的锁,在执行到调用B方法时,可以直接执行而不用再去申请锁。这种情况下我们称之为可重入锁。

2.可中断锁:Lock是可中断锁,synchronized不是,其它线程在等待锁的时候可以中断当前等待就是可中断,说的有点笼统,意思就是当A线程正在使用锁,B线程也本来也想使用锁,但是等的时间有点长了,于是B线程就开始想先做其他事,所以就开始中断等锁这一行为。

3.公平锁:如果有10个线程在等待锁A释放,此时如果锁A释放了,那么这是个线程谁等待的时间久,那么将会获得锁,

synchronized是非公平锁,Lock默认也是非公平锁,但是Lock可以在创建的时候设置。

4.读写锁:读写锁讲一个资源的访问分成了两个锁,读锁和写锁

ReadWriteLock就是一个读写锁,是个接口,子类有:ReenTrantReadWriteLock,可以通过readLock()获取读锁,通过

writeLock()获取写锁

读锁和写锁的互斥关系:

持有读锁时,不能持有写锁

持有写锁时,不能持有读锁

可多线程共同持有读锁,只有单一线程可持有写锁

持有写锁的前提是,没有一个线程持有读锁

持有读锁的前提是,没有一个线程持有写锁

锁升级:读锁--写锁,此处需要先释放读锁

锁降级:写锁--读锁,此处需要先获取读锁,然后再释放写锁(防止其它线程获取写锁后,修改数据,造成脏数据)

(这一段懒得打了,于是就偷了大神的总结,文章结束会将大神博客贴出来)

5.非公平锁:遵循的是线程抢占式调度,没有什么等待时间长,就优先之类的说法

还有几个锁,但是得先理解一下几个概念:

户态和核心态

两个操作系统的概念,用户态,核心态 
(1)当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。 
(2)当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。

线程阻塞的代价 
  java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

  如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间; 
  如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。

为了防止上面出现的高频切换线程而带来的资源损耗问题所以有了下面几个锁:

自旋锁:所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。

目的是为了在短时间内不让一个没有得到锁的线程进行切换,但如果时间一长还是会出现相应的问题的。

 

轻量级锁:自旋锁的目标是降低线程切换的成本。如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。(也就是虽然在多个线程下面,但是每个线程对锁的获取没有竞争,或者说有竞争但是完全可以控制住,这是完全不需要重量级锁来每次都切换线程,所以使用了轻量级锁)

偏向锁:在没有实际竞争的情况下,还能够针对部分场景继续优化。如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗

总结来说:其实我觉得锁这种东西,更像是一种状态,达到某种状态了,给它个名字,然后叫锁了,一个关键字可能会造成几种状态,那么这是他也就会同时有了几种锁,一种状态可能包括了多种状态,比如悲观锁和乐观锁,就可以理解为一种状态,而比如说重量级锁偏向锁这些都属于悲观锁,就可以理解为一种状态中含有了多种状态。(个人理解)

其实锁还有很多,但是按这样理解就容易多了,其实没有刚刚学的时候那么难,就是一个抽象的概念,然后实现就是通过关键字或者参数调节有些本来就是默认这样的,其实没什么,还是那句话千万不要被抽象的概念吓到,很多东西远比想象的简单,只不过底层的知识没学过而已,慢慢地都能会的,加油。今天就到这了,还有几种锁的实现,以及优化,和线程并发时的一些问题明天再总结,明天结束线程。

参考资料:《Java2使用教材(第五版)》

https://www.jianshu.com/p/36eedeb3f912

https://blog.csdn.net/a314773862/article/details/54095819

https://blog.csdn.net/qq_41376740/article/details/80607243

https://www.cnblogs.com/dolphin0520/p/3923167.html

https://blog.csdn.net/qq_35877352/article/details/79415719

https://blog.csdn.net/u013144863/article/details/51392510

https://blog.csdn.net/qq_41247433/article/details/79434202

https://www.cnblogs.com/lcplcpjava/p/6896904.html

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值