学习狂神说JUC并发编程笔记
1.什么是juc?
juc就是java.util.concurrent下的包,专门处理多线程开发。
学习该课程需要对java语言和java多线程编程有一定的了解
2.进程和线程
进程:程序执行的过程。一个程序往往可以包含多个线程(至少包含一个)
线程:是进程的一个执行单元,是进程内调度的实体,比进程更小的独立运行的基本单位。比如一个开启了一个网易云音乐,显示歌词,可能就是一个线程处理的。
java默认有两个线程:GC线程,main线程。
java开启线程的方式
1.继承Thread类,重写run方法
2.实现Runnable接口,重写run方法
3.实现Callable接口,重写call方法
那么java真的可以开启线程吗?
我们知道java在开启一个线程是,需要使用线程类中的start方法
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
//调用start0方法
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();//native关键子是去调用本地方法库中c++的方法
我们发现,他底层调用了start0方法,这个方法有一个native关键字。
如果不熟悉native关键字的含义可以去了解下JVM方面的知识。
3.并发和并行
并发:学习过操作系统的我们都知道cpu是在多个线程之间来回切换,这就是并发,它只是在逻辑上同时发生,其实只是cpu在多个线程之间快速的切换,我们人眼分辨不出来,实际上并不是同时发生。
并行:多个cpu同时操作多个线程,所以这是逻辑上和实际上都是同时发生。这就是并发和并行的区别
并发编程的本质:充分利用cpu资源
说到线程,那么线程有几个装态呢?
我们从操作系统上来说,线程是有5个状态的
1.新建状态
2.就绪状态
3.运行状态
4.阻塞状态
5.死亡状态
那么java中线程的状态是这样的吗?我们通过观察源码发现,java中线程是有6种状态的。
1.新建状态
2.运行状态
3.阻塞状态
4.等待状态
5.超时等待状态
6.死亡状态
口说无凭,我把源码贴在了下方
public enum State {
//这是官方源码,不过我将一些注释删除掉了。
//新建
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//等待
WAITING,
//超时等待 过期不候
TIMED_WAITING,
//终止
TERMINATED;
}
在面试种我们常常会被问到wait和sleep的区别,那么我们来总结一下吧?
- wait方法是来自Object类的,sleep方法是来自Thread类的,我们后面会学习TimeUnit工具类,来进行线程的休眠
- wait方法会释放锁资源,而sleep方法不会释放锁资源
- wait方法必须写在synchronized同步代码块中,但是sleep方法可以在任何地方使用
- 我在网上看其他博客中提到,wait方法不需要捕获异常,sleep方法需要捕获,然后我下去在验证的时候发现wait方法也是需要捕获异常的
Lock锁
我们在前面学习到了,让一个方法变成同步方法需要在方法上面加上synchronized关键字,那么今天我们来学习一种新的方式Lock锁(可重入锁)
首先Lock是一个接口,它下面有一些实现类ReentrantLock,ReentranReadWriteLock(读写锁)等
我们先来看下以前我们解决买票例子的代码吧
package com.mc;
public class Day1 {
public static void main(String[] args) {
B b = new B();
new Thread(()->{
for (int i = 0; i < 60; i++) {
b.getNumber();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 60; i++) {
b.getNumber();
}
},"B").start();
}
}
class B {
private int number = 1;
//使用Synchronized关键字,解决并发问题
public synchronized void getNumber() {
if(number <= 50) {
System.out.println(Thread.currentThread().getName()+"卖出第"+(number++)+"张票");
}
}
}
这样做我们只知道了这个方法加了锁,却不知道,锁的状态,它是给整个方法都加上了锁,如果我想只对代码中一部分代码加锁怎么办?
Lock锁就是为了解决这些问题的
首先Lock锁也是一个可重入锁,那么我们来看下Lock锁是怎样使用的
package com.mc;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo1 {
public static void main(String[] args) {
A a = new A();
new Thread(()->{
for (int i = 0; i < 40; i++) {
a.getNumber();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
a.getNumber();
}
},"B").start();
}
}
class A {
private int number = 30;
public void getNumber() {
//新建锁
Lock lock = new ReentrantLock();
try{
//加锁
lock.lock();
//业务,买票
if(number > 0){
System.out.println(Thread.currentThread().getName()+"卖出了第"+(number--)+"张票");
}
}catch (Exception e) {
e.printStackTrace();
}finally {
//解锁
lock.unlock();
}
}
}
这样是不是可以看出锁的状态了呢?也可以实现局部加锁了
其实在官方文档中,都明确写了这种锁如何使用
所以我们需要学会查阅官方文档
Lock锁还有一个特殊的地方,Synchronized,必须等待一个方法执行完,才会释放锁,但是Lock锁有一个tryLock()方法可以去尝试获取锁
当然Lock锁还有很多用法,比如Synchronized默认是一个非公平锁,那么Lock锁呢?
public ReentrantReadWriteLock() {
this(false);
}
/**
* Creates a new {@code ReentrantReadWriteLock} with
* the given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantReadWriteLock(boolean fair) {
//可以看到Lock锁可以实现公平锁,在创建时传递一个boolean值为true就可以新建出来了
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
那我们来总结一下Lock锁和Synchronized的区别吧。
1.首先Synchronized是一个关键字,Lock锁是一个类。
2.Synchronized无法获取锁的状态,但是Lock锁可以获得锁的状态
3.synchronized 线程1(获得锁、阻塞) 线程2(等待);Lock锁不一定会 lock.tranlock()尝试获得锁
4.synchronized关键字不需要手动释放锁,但是lock锁必须手动释放锁。
5.synchronized是可重入锁,非公平的,lock锁是可重入锁,可以公平也可以不公平(默认非公平)
6.Synchronized适合少量的代码同步问题,Lock适合锁大量的同步代码!
有一个疑问,关于锁是谁,对于lock锁我们很好分辨,但是对于synchronized呢,我们如何去判断,这里大家可以去练习一下8锁问题,小提示,谁调用方法,谁就是锁。
生产者消费者问题
synchronized是怎样写的
package com.mc;
//生产者消费者问题synchronized
public class Demo2 {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(()->{
for(int i=0;i<20;i++){
try {
resource.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for(int i=0;i<20;i++){
try {
resource.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for(int i=0;i<20;i++){
try {
resource.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for(int i=0;i<20;i++){
try {
resource.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
class Resource {
private int number = 0;
//生产者
public synchronized void increment() throws InterruptedException {
//判断
while (number != 0){
wait();
}
//业务
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
this.notifyAll();
}
//消费者
public synchronized void decrement() throws InterruptedException {
while (number==0){
wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
this.notifyAll();
}
}
注意:有的小伙伴在判断条件时,使用if条件,但是这样有可能会引发问题,有些面试官也特别爱问这一点。官方文档也给我们解释了为什么使用while而不是if
那接下来看看我们Lock版的。
在看Lock版之前,我们发现synchronized是使用了synchronized和wait以及notifyAll来完成的,那么我们lock怎样解决的呢?
我们lock中有一个方法可以获取一个condition对象,这个对象中有两个方法来解决这个问题
package com.mc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo4 {
public static void main(String[] args) {
Data1 data1 = new Data1();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data1.a();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data1.b();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data1.c();
}
},"C").start();
}
}
class Data1{
private int number = 1;
private Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void a(){
lock.lock();
try {
while (number!=1){
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"=>"+number);
number = 2;
condition2.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void b(){
lock.lock();
try {
while (number!=2){
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"=>"+number);
number = 3;
condition3.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void c(){
lock.lock();
try {
while (number!=3){
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"=>"+number);
number = 1;
condition1.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
这块我们使用了精确唤醒 A->B->C->A 这就是lock版的强大
集合不安全
首先我们在平时使用的arraylist,hashset,hashmap都是线程不安全的,这点我们在平时学习的时候也都知道。
当然我们也可以使用一些集合安全的类来解决它
如vector、hashtable
或者使用工具类Collection.synchronizedxxxx(list or set or map)方法来让集合变安全。
那么我们juc是干什么的,是并发编程,那么他如何来解决这些问题呢?
首先juc下面有一些集合类来解决这些问题
1.CopyOnWriteArrayList
写入时赋值 CopyOnWrite
核心思想是,如果有多个调用者同时要求相同的资源(如内存或者磁盘上的数据存储),它们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本给调用者,而其他调用者所见到的最初资源任然保持不变。这过程对其他的调用者都是透明的。此做法主要的优点是如果调用者没有修改资源,就不会有副本被创建,因此多个调用者只是读取操作时可以共享同一份资源
可以理解为,读的时候不加锁,只要在写的时候才会加锁,并赋值一份新的文件,让该线程去操作
2.CopyOnWriteArraySet
3.ConcurrentHashMap
底层使用了CAS,和Synchronized来解决线程安全问题
使用了volatile来保证可见性和禁止指令重排的。
CAS和volatile后面会讲
就是解决这些问题的,并且效率更高
我在这里只介绍两种,有兴趣的朋友可以下去自己研究。
参考:狂神说juc并发编程视频