指的是java.util包下的三个工具类:
实现多线程的三种方式:
Runnable没有返回值、效率相比于Callable相对较低!
业务:普通的线程代码 Thread
本篇笔记篇幅较长,若想一气呵成地看完就看本篇就行。如果想一点点地看,把其中的部分抽取出来做成了4个子笔记,点击链接看对应子笔记就行。两种方式的内容相同。
1.什么是JUC?(内容较少,就未抽取)
2.java线程和进程演绎法5919:java线程和进程学习子笔记
3.Lock锁中的synchonized部分演绎法5919:synchronized学习笔记
4.8锁现象演绎法5919:8锁现象学习子笔记
5.不安全的集合类演绎法5919:不安全的集合类学习子笔记
进程:是一个程序,一个进程包含多个线程,且至少包含一个。
Java默认有两个线程:main 和 GC。Java是不能开启线程的,底层是调用start0()是一个native方法,由底层的C++方法编写。java无法直接操作硬件。
并发:CPU单核,多个线程共用一个资源,快速交替方式达到并行的假象。本质->充分利用cpu资源。
并发:CPU多核,多个线程同时执行
System.out.println(Runtime.getRuntime().availabelProcessors());//获取cpu核数
线程有几个状态:
public enum State{
NEW,//新生
RUNNABLE,//运行
BLOCKED,//阻塞
BLOCKED,//等待
TIMED_WAITING,//超时等待
TERMINATED;//终止
}
wait和sleep的区别
wait:来自Object类,会释放锁,必须在同步代码块中使用,不需要捕获异常
让当前线程进入等待状态,当别的其他线程调用notify()或者notifyAll()方法时,当前线程进入就绪状态。wait方法必须在同步上下文中调用。也就是说,如果想要调用wait方法,前提是必须获取对象上的锁资源。当wait方法调用时,当前线程会释放已获取的对象锁资源,并进入等待队列,其他线程就可以尝试获取对象上的锁资源。
sleep:来自Thread类是一个静态方法,不会释放锁!(睡着了),可以在任何地方使用,必须要捕获异常
让当前线程休眠指定时间。休眠时间的准确性依赖于系统时钟和CPU调度机制。不释放以获取的锁资源,如果sleep方法在同步上下文中调用,那么其他线程是无法进入当前同步快或者同步方法中的。可通过interrupt()方法来唤醒休眠线程。
3.1传统的synchronized锁:队列锁
1.sychronized的作用:
在并发编程中会存在线程安全问题,主要原因是存在共享数据和多线程共同操作共享数据。关键字sychronized可以保证在同一时刻只有一个线程可以执行某个方法或某个代码块(临界区),同时synchronized可以保证一个线程的变化可见。
只有共享资源读写访问才需要同步化,如果不是共享资源就没有必要同步。
2.三种应用方式
2.1同步方法(静态方法、实例方法)
用sychronized修饰的方法就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
synchronized public void doWorlk(){
...
}
对于非静态类方法,同步锁就是this(实例对象)
对于静态类方法,同步锁是当前方法所在类的字节码对象(类对象)
2.2同步代码块
syschronized(同步锁){
....
}
同步锁:在任何时候最多只能有一个线程有同步锁,其他线程只能在代码块外等着。
为保证每个线程都能正常执行原子操作,Java引入了线程同步机制。java程序运行使用任何对象作为同步监听对象,但一般的,我们把当前并发访问的共同资源作为同步监听对象。
3.举个栗子:
package demo01;
public class test01 {
public static void main(String[] args) throws InterruptedException {
// 并发:多线程操作同一个资源类,
Ticket ticket = new Ticket();
// @FunctionalInterface 函数式接口
new Thread(() -> {
for (int i = 1; i < 20; i++) {
//把资源类丢入线程
ticket.sale();
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i < 20; i++) {
ticket.sale();
}
}, "C").start();
}
}
class Ticket {
//共卖40张票
private int number = 30;
// 卖票的方式
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "票,剩余:" + number);
}
}
}
当多个线程同时对一个对象的一个方法进行操作,只有一个线程能够抢到锁。因为一个对象只有一把锁,一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,就不能访问该对象的其他synchronized实例方法,但是可以访问非synchronized修饰的方法。
3.2Lock接口
Lock所是一个接口,其所有的实现类为
Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。Lock接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个所。随着灵活的增加,也带来了更多的责任。不使用块结构锁就失去了使用synchronized方法和语句时会出现的锁自动释放功能。在大多数情况下,应使用以下语句:
Lock l = ...;
l.lock();//加锁
try {
// access the resource protected by this lock
} finally {
l.unlock();//解锁
}
加锁和解锁出现在不同作用范围中是,需谨慎确保锁定是所执行的所有代码用try-finally或try-catch保护,以确保在必要时释放锁。
公平锁:十分公平,遵循先来后到
非公平锁:十分不公平,出现插队现象
可重入锁:如果锁具备可重入性,则称为可(可以)重(再次)入(进入同步域,即同步代码块/方法)锁(同步锁)。可重入就是指某个线程已经获得某个锁,可以再次获取相同的锁而不会出现死锁。
3.3synchronized和Lock锁的区别
synchronized:
Lock:
掌握8中锁的现象永远知道锁的是谁!
package lock8;
import java.util.concurrent.TimeUnit;
public class Test01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.call();
}, "A").start();
new Thread(() -> {
phone.send();
}, "B").start();
}
}
class Phone {
public synchronized void send() {
System.out.println("发短信");
}
public synchronized void call() {
//现象二时添加,让线程先睡4秒种
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
}
现象一:两个方法都使用synchronized关键字,先执行打电话
现象二:让线程先睡4秒,结果依然是先打电话
原因:synchromized锁的是方法的调用者,并且开启的两个线程方法使用的是同一把锁,那么就会出现谁先拿到谁先执行的现象。及时我们让call方法sleep了4秒,依然是call方法先执行。
package lock8;
import java.util.concurrent.TimeUnit;
public class Test02 {
public static void main(String[] args) {
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
new Thread(() -> {
phone1.call();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
// phone1.hello();
phone2.send();
}, "B").start();
}
}
class Phone2 {
public synchronized void send() {
System.out.println("发短信");
}
public synchronized void call() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
public void hello() {
System.out.println("hello");
}
}
现象三:在之前代码的基础上,新添加一个普通的方法,此时先执行hello方法
现象四:新实例化一个phone对象,使用不同的对象去调用方法,此时先执行发短信
原因:普通方法没有锁就不是同步方法不受锁的影响,又由于时间的延迟,所以先打印hello.现象四中,使用了不同的对象,以至于是锁的对象不是同一个,所以先发短信。
package lock8;
import java.util.concurrent.TimeUnit;
public class Test03 {
public static void main(String[] args) {
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
new Thread(() -> {
phone1.call();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
//phone1.send();
phone2.send();
}, "B").start();
}
}
class Phone3 {
public static synchronized void send() {
System.out.println("发短信");
}
public static synchronized void call() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
}
现象五:在之前代码的基础上,方法前添加static关键字,先执行打电话
现象六:再添加一个对象,使用不同的对象进行方法的打印,依然是先执行打电话
原因:当我们添加了static关键字以后,此时我们先打印打电话,不仅是因为我们拿到的是同一个对象的锁,还因为我们的锁是直接锁的该类的Class模板。当我们再新添加一个对象时,由于我们使用了static,直接锁在模板上,所以依然是先执行打电话
package lock8;
import java.util.concurrent.TimeUnit;
public class Test04 {
public static void main(String[] args) {
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();
new Thread(() -> {
phone1.call();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
// phone1.send();
phone2.send();
}, "B").start();
}
}
class Phone4 {
public synchronized void send() {
System.out.println("发短信");
}
public static synchronized void call() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打电话");
}
}
现象七:当去掉一个锁方法的static关键字以后,先打印发短信
现象八:当我们新建一个对象后,是有不同的对象去调用,还是先打印发短信
原因:现象7中,只添加了一个static关键字,模板只有一部分被锁,则先打印发短信。现象8中,又新建了一个对象,因为锁的东西不一样(打电话锁模板,发短信锁对象),所以先执行发短信。
只有当锁的对象或模板时同一个的时候,才能借助调用的顺序来执行。
不安全类是什么?
不安全类是指在多线程并发的时候不能保证数据正确性的类,通常是由于这些类并没有加锁造成的。
为什么不设计成加锁的?
其实,在list之前有个集合类vector,它是内部加锁,它是一个线程安全类。不优先使用它的原因是加锁可以保证数据的正确性,但却降低了并发效率。list单线程安全,多线程不安全。并发条件下会产生ConcurrentModificationException异常(并发修改异常)
如何做到保证数据的正确性呢?
CopyOnWriteArrayList 写时复制,读写分离
既能保证数据的正确性,又不会使并发效率变低。它的add源码:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
复制一个原来数组副本,在副本里“写入元素”,最后再用写完的副本替换原本的数组,即我们在
“写”时可以“读”,这两个操作所使用的不是一个数组,不会产生影响。
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
这样写时复制实现了读写分离,我们不需要在读的时候加锁(之前读需要加锁是因为读写不能同时进行,但一旦给读加了锁,那么读也不能同时进行,就降低了并发效率)
但是,我们每“写入”一个元素就要复制扩容一次数组,是非常耗时耗资源的,所以当我们需要写入较多数据的时候,CopyOnArrayList就不那么合适了。
也是不安全的集合类
将不安全的集合变成安全集合的方法:
1.Set set = Collections.synchronizedSet(new HashSet<>());
2.Set set = new CopyOnWriteArrayListSet<>();
HashSet底层是什么?
//默认的空参初始化方法
public HashSet() {
map = new HashMap<>();
}
//使用HashSet的add方法,依然是调用HashMap的底层put方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
也就是说HashSet的底层就是HashMap
Map
也是不安全的集合类
变成安全的方法:
1.Map
无论读取还是写入,都会进行加锁,当并发级别特别高,线程之间在任何操作上都会进行等待,效率低。
2.Map
采用分段锁技术,其中Segment继承于ReentrantLock。不会像HashTable(线程安全) 那样不管是put还是get操作都需要做同步处理,理论上ConcurrentHashMap支持CurrentLevel(Segment数组数量)的线程并发。每当一个线程占用锁访问一个Segment时,不会影响其他的Segment.
参考博客:
JUC(一)_默辨的博客-CSDN博客
Java中的sleep与wait区别_helloworld的专栏-CSDN博客
可重入锁详解(什么是可重入)_w8y56f的专栏-CSDN博客
不安全的集合类list以及解决方法_qq_17690301的博客-CSDN博客
【Java并发编程之深入理解】Synchronized的使用_青苔小榭-CSDN博客
synchronized理解_蓝胖子bot的博客-CSDN博客
[java] synchronized关键字用法及实现原理详解