在 Java 5.0 提供了 java.util.concurrent
(简称JUC)包,在此包中增加了在并发编程中很常用的工具类。此包包括了几个小的、已标准化的可扩展框架,并提供一些功能实用的类,没有这些类,一些功能会很难实现或实现起来冗长乏味。
进程:cpu进行资源分配的基本单位
线程:cpu进行独立调度的基本单位
并行:多项工作一起执行、之后再汇总
并发:同一时刻多个线程访问同一资源
wait:暂停当前线程,释放锁-------------Object的方法
sleep:暂停当前线程,不释放锁--------Thread的方法
创建线程常用两种方式:
继承Thread抽象类:
public class MyThread extends Thread
new MyThread().start();
实现Runnable接口的方式:
class MyRunnable implements Runnable//新建类实现runnable接口
new Thread(new MyRunnable(), name).start // 使用Rannable实现类创建进程,name是线程名
new Thread(new Runnable() {
@Override
public void run() {
// 调用资源方法,完成业务逻辑
}
}, "your thread name").start();
之前说了Runnable接口的两种实现方式,其实还有第三种:
new Thread(() -> {
}, "your thread name").start();
Lambda 是一个匿名函数,我们可以把 Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
Lambda 表达式在Java 语言中引入了一个新的语法元素和操作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符或剪头操作符。它将 Lambda 分为两个部分:
传统写法
interface Foo {
public int add(int x, int y);
}
public class LambdaDemo {
public static void main(String[] args) {
Foo foo = new Foo() {
@Override
public int add(int x, int y) {
return x + y;
}
};
System.out.println(foo.add(10, 20));
}
}
使用lambda表达式,改造main方法
public static void main(String[] args) {
Foo foo = (int x, int y)->{
return x + y;
};
System.out.println(foo.add(10, 20));
}
改造口诀:拷贝小括号(),写死右箭头->,落地大括号{...}
思考:如果Foo接口有多个方法,还能使用lambda表达式吗?
能,但是只能有一个抽象方法
lambda表达式,必须是函数式接口,必须只有一个抽象方法,如果接口只有一个抽象方法java默认它为函数式接口。
为了正确使用Lambda表达式,需要给接口加个注解:@FunctionalInterface。如有两个方法,立刻报错。
Runnable接口为什么可以用lambda表达式?
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface Runnable
is used
* to create a thread, starting the thread causes the object's
* run
method to be called in that separately executing
* thread.
*
* The general contract of the method run
is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
发现Runnable接口上有一个注解:@FunctionalInterface
并且该接口只有一个方法:run()方法
其实,函数式接口必须只有一个方法,这个描述并不准确,它还允许有default方法和静态方法。
例如,在Foo接口中,又添加了sub方法和mul方法:
interface Foo {
public int add(int x, int y); // 抽象方法
default int sub(int x, int y){ // default方法
return x - y;
}
public static int mul(int x, int y){ // 静态方法
return x * y;
}
}
public class LambdaDemo {
public static void main(String[] args) {
Foo foo = (int x, int y)->{ // lambda表达式实现抽象方法
return x + y;
};
System.out.println(foo.add(10, 20)); // 调用抽象方法
System.out.println(foo.sub(30, 15)); // 调用default方法
System.out.println(Foo.mul(10, 50)); // 通过Foo调用静态方法
}
}
lambda表达式实现接口的前提是
有且只有一个抽象方法,可以选择@FunctionalInterface注解增强函数式接口定义
改造口诀
拷贝小括号(形参列表),写死右箭头 ->,落地大括号 {方法实现}
多线程编程模板上
:
线程 操作 资源类
实现步骤:
例子:卖票程序
package juc;
/**
* @author:嘟嘟
* @date:2022/8/6
*/
class Ticket{
private Integer number=10;
public synchronized void sale(){
if (number<=0){
System.out.println("抱歉、票已售完");
return;
}
try {
System.out.println(Thread.currentThread().getName()+"开始买票,当前剩余"+number+"张");
Thread.sleep(20);
number--;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 20; i++) {
ticket.sale();
}
},"AAA").start();
new Thread(()->{
ticket.sale();
},"BBB").start();
new Thread(()->{
ticket.sale();
},"CCC").start();
}
}
案例省略
总结:
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式:
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
也就是说:
如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁;可是不同实例对象的非静态同步方法因为用的是不同对象的锁,所以毋须等待其他实例对象的非静态同步方法释放锁,就可以获取自己的锁。
所有的静态同步方法用的是同一把锁——类对象本身。不管是不是同一个实例对象,只要是一个类的对象,一旦一个静态同步方法获取锁之后,其他对象的静态同步方法,都必须等待该方法释放锁之后,才能获取锁。
而静态同步方法(Class对象锁)与非静态同步方法(实例对象锁)之间是不会有竞态条件的。
首先看一下JUC的重磅武器——锁(Lock)
相比同步锁,JUC包中的Lock锁的功能更加强大,它提供了各种各样的锁(公平锁,非公平锁,共享锁,独占锁……),所以使用起来很灵活。
翻译过来就是:
锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。
Lock是一个接口,这里主要有三个实现:ReentrantLock、ReentrantReadWriteLock.ReadLock、ReentrantReadWriteLock.WriteLock
使用ReentrantLock改造卖票程序:只需改造sale()方法
package juc;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author:嘟嘟
* @date:2022/8/6
*/
class Ticket{
private Integer number=10;
private ReentrantLock lock = new ReentrantLock();
public synchronized void sale(){
lock.lock();
if (number<=0){
System.out.println("抱歉、票已售完");
lock.unlock();
return;
}
try {
System.out.println(Thread.currentThread().getName()+"开始买票,当前剩余"+number+"张");
Thread.sleep(20);
number--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 20; i++) {
ticket.sale();
}
},"AAA").start();
new Thread(()->{
ticket.sale();
},"BBB").start();
new Thread(()->{
ticket.sale();
},"CCC").start();
}
}
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
例如下列伪代码:
class A{
public synchronized void aa{
......
bb();
......
}
public synchronized void bb{
......
}
}
A a = new A();
a.aa();
A类中有两个普通同步方法,都需要对象a的锁。如果是不可重入锁的话,aa方法首先获取到锁,aa方法在执行的过程中需要调用bb方法,此时锁被aa方法占有,bb方法无法获取到锁,这样就会导致bb方法无法执行,aa方法也无法执行,出现了死锁情况。可重入锁可避免这种死锁的发生。
package juc;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author:嘟嘟
* @date:2022/8/6
*/
class Ticket{
private Integer number=10;
private ReentrantLock lock = new ReentrantLock();
public synchronized void sale(){
lock.lock();
if (number<=0){
System.out.println("抱歉、票已售完");
lock.unlock();
return;
}
try {
System.out.println(Thread.currentThread().getName()+"开始买票,当前剩余"+number+"张");
Thread.sleep(20);
number--;
this.check();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
//测试可重入锁
public void check(){
lock.lock();
System.out.println("检查票数。。。");
lock.unlock();
}
}
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 20; i++) {
ticket.sale();
}
},"AAA").start();
new Thread(()->{
ticket.sale();
},"BBB").start();
new Thread(()->{
ticket.sale();
},"CCC").start();
}
}
可以发现程序可以正常执行。。。说明该锁确实可重入。
ReentrantLock还可以实现公平锁。所谓公平锁,也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。
private ReentrantLock lock = new ReentrantLock(true);
这个是什么意思呢?也就是通过我们的tryLock方法来实现,可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以将这种方法用来解决死锁问题。
(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断。
在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用java提供的关键字synchronized或者concurrents包中实现了Lock接口的ReentrantLock。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。而在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
读写锁的特点:
package juc;
import java.util.HashMap;
import java.util.Map;
/**
* @author:嘟嘟
* @date:2022/8/8
*/
class MyCache{
private volatile Map<String, String> cache= new HashMap<>();
public void put(String key, String value){
try {
System.out.println(Thread.currentThread().getName() + " 开始写入!");
Thread.sleep(300);
cache.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入成功!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
}
public void get(String key){
try {
System.out.println(Thread.currentThread().getName() + " 开始读出!");
Thread.sleep(300);
String value = cache.get(key);
System.out.println(Thread.currentThread().getName() + " 读出成功!" + value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
}
}
public class ReentrantReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
String number = String.valueOf(i);
new Thread(()->{
myCache.put(number,number);
},number).start();
}
for (int i = 1; i <= 5; i++) {
String number = String.valueOf(i);
new Thread(()->{
myCache.get(number);
},number).start();
}
}
}
改造MyCache,加入读写锁:
class MyCache{
private volatile Map<String, String> cache= new HashMap<>();
ReentrantReadWriteLock writeLock = new ReentrantReadWriteLock();
public void put(String key, String value){
//加入写锁
writeLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 开始写入!");
Thread.sleep(300);
cache.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入成功!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放写锁
writeLock.writeLock().unlock();
}
}
public void get(String key){
//加入读锁
writeLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 开始读出!");
Thread.sleep(300);
String value = cache.get(key);
System.out.println(Thread.currentThread().getName() + " 读出成功!" + value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放读锁
writeLock.readLock().unlock();
}
}
}
什么是锁降级,锁降级就是从写锁降级成为读锁。在当前线程拥有写锁的情况下,再次获取到读锁,随后释放写锁的过程就是锁降级。这里可以举个例子:
public void test(){
rwlock.writeLock().lock();
System.out.println("获取到写锁。。。。");
rwlock.readLock().lock();
System.out.println("获取到读锁----------");
rwlock.writeLock().unlock();
System.out.println("释放写锁==============");
rwlock.readLock().unlock();
System.out.println("释放读锁++++++++++++++++");
}
打印效果:
锁降级的用途: 锁分为读锁 (共享锁)、写锁(排他锁)两种:
一个线程获取了写锁,其他线程无法获取写锁、读锁,进行阻塞; 一个线程获取了读锁,其他线程无法获取写锁(进行阻塞),但是可以获取读锁;
如果只使用写锁,那么释放写锁之后,其他线程就会获取到写锁或读锁,使用锁降级可以在释放写锁前获取读锁,这样其他的线程就只能获取读锁,对这个数据进行读取,但是不能获取写锁进行修改,只有当前线程释放了读锁之后才可以进行修改。锁降级的意义是当前线程由write降为read,其他只需要read锁的线程可以去获取read锁,达到并发读的目的。
支持公平/非公平策略
支持可重入
支持锁降级,不支持锁升级
在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。
Condition条件支持
写锁可以通过newCondition()
方法获取Condition对象。但是读锁是没法获取Condition对象,读锁调用newCondition()
方法会直接抛出UnsupportedOperationException
。
面试题:两个线程打印
两个线程,一个线程打印1-52,另一个打印字母A-Z打印顺序为12A34B…5152Z,要求用线程间通信
先来简单案例:
两个线程操作一个初始值为0的变量,实现一个线程对变量增加1,一个线程对变量减少1,交替10轮。
线程间通信模型:
多线程编程模板中:
package juc;
/**
* @author:嘟嘟
* @date:2022/8/8
*/
class ShareDataOne{
private Integer number=0;
public synchronized void increment() throws InterruptedException {
if (number!=0){
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+":"+number);
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
if (number!=1){
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+":"+number);
this.notifyAll();
}
}
public class NotifyWaitDemo {
public static void main(String[] args) {
ShareDataOne dataOne = new ShareDataOne();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
dataOne.increment();
}catch (Exception e){
e.printStackTrace();
}
}
},"AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
dataOne.decrement();
}catch (Exception e){
e.printStackTrace();
}
}
},"BBB").start();
}
}
部分打印结果:
如果换成4个线程会怎样?
改造mian方法,加入CCC和DDD两个线程:
public class NotifyWaitDemo {
public static void main(String[] args) {
ShareDataOne dataOne = new ShareDataOne();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
dataOne.increment();
}catch (Exception e){
e.printStackTrace();
}
}
},"AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
dataOne.decrement();
}catch (Exception e){
e.printStackTrace();
}
}
},"BBB").start();
new Thread(()->{
try {
dataOne.increment();
}catch (Exception e){
e.printStackTrace();
}
},"CCC").start();
new Thread(()->{
try {
dataOne.decrement();
}catch (Exception e){
e.printStackTrace();
}
},"DDD").start();
}
}
打印结果,依然会有概率是,10101010…。
但是,多执行几次,也会出现错乱的现象:
换成4个线程会导致错误,虚假唤醒
原因:在java多线程判断时,不能用if,程序出事出在了判断上面。
注意,消费者被唤醒后是从wait()方法(被阻塞的地方)后面执行,而不是重新从同步块开头。
如下图:
解决虚假唤醒:查看API,java.lang.Object的wait方法
中断和虚假唤醒是可能产生的,所以要用loop循环,if只判断一次,while是只要唤醒就要拉回来再判断一次。
if换成while
package juc;
/**
* @author:嘟嘟
* @date:2022/8/8
*/
class ShareDataOne{
private Integer number=0;
public synchronized void increment() throws InterruptedException {
while (number!=0){
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+":"+number);
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
while (number!=1){
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+":"+number);
this.notifyAll();
}
}
public class NotifyWaitDemo {
public static void main(String[] args) {
ShareDataOne dataOne = new ShareDataOne();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
dataOne.increment();
}catch (Exception e){
e.printStackTrace();
}
}
},"AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
dataOne.decrement();
}catch (Exception e){
e.printStackTrace();
}
}
},"BBB").start();
new Thread(()->{
try {
dataOne.increment();
}catch (Exception e){
e.printStackTrace();
}
},"CCC").start();
new Thread(()->{
try {
dataOne.decrement();
}catch (Exception e){
e.printStackTrace();
}
},"DDD").start();
}
}
再次测试,完美解决
对标synchronized:
Condition:查看API,java.util.concurrent.locks
并提供了实现案例:
使用Condition实现线程通信,改造之前的代码(只需要改造ShareDataOne):删掉increment和decrement方法的synchronized
package juc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author:嘟嘟
* @date:2022/8/8
*/
class ShareDataOne{
private Integer number=0;
final Lock lock = new ReentrantLock(); // 初始化lock锁
final Condition condition = lock.newCondition(); // 初始化condition对象
public void increment() throws InterruptedException {
lock.lock();
try {
while (number!=0){
//this.wait();
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+":"+number);
//this.notifyAll();
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number!=1){
//this.wait();
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+":"+number);
//this.notifyAll();
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class NotifyWaitDemo {
public static void main(String[] args) {
ShareDataOne dataOne = new ShareDataOne();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
dataOne.increment();
}catch (Exception e){
e.printStackTrace();
}
}
},"AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
dataOne.decrement();
}catch (Exception e){
e.printStackTrace();
}
}
},"BBB").start();
new Thread(()->{
try {
dataOne.increment();
}catch (Exception e){
e.printStackTrace();
}
},"CCC").start();
new Thread(()->{
try {
dataOne.decrement();
}catch (Exception e){
e.printStackTrace();
}
},"DDD").start();
}
}
案例:
多线程之间按顺序调用,实现A->B->C。三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次
接着
AA打印5次,BB打印10次,CC打印15次
。。。打印10轮
分析实现方式:
演示执行一轮
package juc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author:嘟嘟
* @date:2022/8/8
*/
class ShareDataTwo{
private Integer flag=1;
private final Lock lock = new ReentrantLock();
private final Condition condition1 = lock.newCondition();
private final Condition condition2 = lock.newCondition();
private final Condition condition3 = lock.newCondition();
public void print5(){
lock.lock();
try {
while (flag!=1){
condition1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":AAA");
}
flag=2;
condition2.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print10(){
lock.lock();
try {
while (flag!=2){
condition2.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+"BBB");
}
flag=3;
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print15(){
lock.lock();
try {
while (flag!=3){
condition3.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName()+":"+"CCC");
}
flag=1;
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class ThreadOrderAccess {
public static void main(String[] args) {
ShareDataTwo shareDataTwo = new ShareDataTwo();
new Thread(()->{
shareDataTwo.print5();
},"AAA").start();
new Thread(()->{
shareDataTwo.print10();
},"BBB").start();
new Thread(()->{
shareDataTwo.print15();
},"CCC").start();
}
}
面试题:
请举例说明集合类是不安全的。
首先以List作为演示对象,创建多个线程对List接口的常用实现类ArrayList进行add操作。
public class NotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
出现了线程不安全错误
ArrayList在多个线程同时对其进行修改的时候,就会抛出java.util.ConcurrentModificationException异常(并发修改异常),因为ArrayList的add及其他方法都是线程不安全的,有源码佐证:
解决方案:
List接口有很多实现类,除了常用的ArrayList之外,还有Vector和SynchronizedList。
他们都有synchronized关键字,说明都是线程安全的。
改用Vector或者synchronizedList试试:
public class NotSafeDemo {
public static void main(String[] args) {
//List list = new ArrayList<>();
List<String> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
即可解决!
Vector和Synchronized的缺点:
vector:内存消耗比较大,适合一次增量比较大的情况
SynchronizedList:迭代器涉及的代码没有加上线程同步代码
什么是CopyOnWrite容器 数据一致性问题
CopyOnWrite容器(简称COW容器)即**写时复制
**的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy
,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器
。这样做的好处是我们可以对CopyOnWrite容器进行并发的读
,而不需要加锁
,因为当前容器不会添加任何元素。所以 CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。
先看看CopyOnWriteArrayList类:发现它的本质就是数组
再来看看CopyOnWriteArrayList的add方法:发现该方法是线程安全的
使用CopyOnWriteArrayList改造main方法:
public class NotSafeDemo {
public static void main(String[] args) {
//List list = new ArrayList<>();
//List list = Collections.synchronizedList(new ArrayList<>());
List<String> list= new CopyOnWriteArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
CopyOnWrite并发容器用于读多写少的并发场景。比如:白名单,黑名单。假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单一定周期才会更新一次。
缺点:
通过压缩容器中的元素的方法来减少大对象的内存消耗
,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。最终一致性,不能保证数据的实时一致性
。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。HashSet和HashMap也都是线程不安全的,类似于ArrayList,也可以通过代码证明。
private static void notSafeMap() {
Map<String, String> map = new HashMap<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
map.put(String.valueOf(Thread.currentThread().getName()), UUID.randomUUID().toString().substring(0, 8));
System.out.println(map);
}, String.valueOf(i)).start();
}
}
private static void notSafeSet() {
Set<String> set = new HashSet<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(set);
}, String.valueOf(i)).start();
}
}
都会报:ConcurrentModificationException异常信息。
Collections提供了方法synchronizedList保证list是同步线程安全的,Set和Map呢?
JUC提供的CopyOnWrite容器实现类有:CopyOnWriteArrayList和CopyOnWriteArraySet。
有没有Map的实现:
最终实现:
public class NotSafeDemo {
public static void main(String[] args) {
notSafeList();
notSafeSet();
notSafeMap();
}
private static void notSafeMap() {
//Map map = new HashMap<>();
//Map map = Collections.synchronizedMap(new HashMap<>());
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
map.put(String.valueOf(Thread.currentThread().getName()), UUID.randomUUID().toString().substring(0, 8));
System.out.println(map);
}, String.valueOf(i)).start();
}
}
private static void notSafeSet() {
//Set set = new HashSet<>();
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(set);
}, String.valueOf(i)).start();
}
}
private static void notSafeList() {
//List list = new Vector<>();
//List list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
扩展:HashSet底层数据结构是什么?HashMap ?
但HashSet的add是放一个值,而HashMap是放K、V键值对
JUC的多线程辅助类非常多,这里我们介绍三个:
CountDownLatch是一个非常实用的多线程控制工具类,应用非常广泛。
例如:在手机上安装一个应用程序,假如需要5个子进程检查服务授权,那么主进程会维护一个计数器,初始计数就是5。用户每同意一个授权该计数器减1,当计数减为0时,主进程才启动,否则就只有阻塞等待了。
CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。CountDownLatch的作用也是如此。
常用的就下面几个方法:
new CountDownLatch(int count) //实例化一个倒计数器,count指定初始计数
countDown() // 每调用一次,计数减一
await() //等待,当计数减到0时,阻塞线程(可以是一个,也可以是多个)并行执行
案例:6个同学陆续离开教室后值班同学才可以关门。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 初始化计数器,初始计数为6
CountDownLatch count = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println("第"+Thread.currentThread().getName()+"个同学已出门");
// 调用countDown()计算减1
count.countDown();
},String.valueOf(i)).start();
}
// 调用计算器的await方法,等待6位同学都出来
count.await();
System.out.println("同学都出来了、可以锁门了");
}
}
面试:CountDownLatch 与 join 方法的区别
调用一个子线程的 join()方法后,该线程会一直被阻塞直到该线程运行完毕。而 CountDownLatch 则使用计数器允许子线程运行完毕或者运行中时候递减计数,也就是 CountDownLatch 可以在子线程运行任何时候让 await 方法返回而不一定必须等到线程结束;另外使用线程池来管理线程时候一般都是直接添加 Runnable 到线程池这时候就没有办法在调用线程的 join 方法了,countDownLatch 相比 Join 方法让我们对线程同步有更灵活的控制。
从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。该命令只在每个屏障点运行一次。若在所有参与线程之前更新共享状态,此屏障操作很有用
常用方法:
案例:组队打boss过关卡游戏。
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3,()->{
System.out.println(Thread.currentThread().getName()+"过关了");
});
for (int i = 0; i < 3; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName() + " 开始第一关");
TimeUnit.SECONDS.sleep(new Random().nextInt(4));
System.out.println(Thread.currentThread().getName() + " 开始打boss");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " 开始第二关");
TimeUnit.SECONDS.sleep(new Random().nextInt(4));
System.out.println(Thread.currentThread().getName() + " 开始打boss");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " 开始第三关");
TimeUnit.SECONDS.sleep(new Random().nextInt(4));
System.out.println(Thread.currentThread().getName() + " 开始打boss");
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
注意:所有的"过关了"都是由最后到达await方法的线程执行打印的
面试:CyclicBarrier和CountDownLatch的区别?
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景;CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置。
Semaphore翻译成字面意思为 信号量,Semaphore可以控制同时访问的线程个数。非常适合需求量大,而资源又很紧张的情况。比如给定一个资源数目有限的资源池,假设资源数目为N,每一个线程均可获取一个资源,但是当资源分配完毕时,后来线程需要阻塞等待,直到前面已持有资源的线程释放资源之后才能继续。
常用方法:
public Semaphore(int permits) // 构造方法,permits指资源数目(信号量)
public void acquire() throws InterruptedException // 占用资源,当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
public void release() // (释放)实际上会将信号量的值加1,然后唤醒等待的线程。
信号量主要用于两个目的:
案例:6辆车抢占3个车位
public class SemaphoreDemo {
public static void main(String[] args) {
// 初始化信号量,3个车位
Semaphore semaphore = new Semaphore(3);
// 6个线程,模拟6辆车
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到一个车位");
Thread.sleep(200);
System.out.println(Thread.currentThread().getName()+"离开车位");
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
Thread类、Runnable接口使得多线程编程简单直接。
但Thread类和Runnable接口都不允许声明检查型异常
,也不能定义返回值**。没有返回值这点稍微有点麻烦。不能声明抛出检查型异常则更麻烦一些。
**public void run()**方法规范意味着你必须捕获并处理检查型异常。即使你小心捕获异常,也不能保证这个类(Runnable对象)的所有使用者都读取异常信息。
以上两个问题现在都得到了解决。从java5开始,提供了Callable接口,是Runable接口的增强版。用Call()方法作为线程的执行体,增强了之前的run()方法。因为call方法可以有返回值,也可以声明抛出异常。
先初步认识一下Callable接口:
这是一个函数式接口,因此可以用作lambda表达式或方法引用的赋值对象。
具体代码实现对比:创建一个CallableDemo.java
class MyRunnableThread implements Runnable{
@Override
public void run() {
}
}
class MyCallableThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return null;
}
}
public class CallableDemo {
public static void main(String[] args) {
// 创建多线程
}
}
该如何使用Callable创建Thread对象,如果使用Runnable是:
public class CallableDemo {
public static void main(String[] args) {
// 创建多线程
new Thread(new MyRunnableThread(), "threadName").start();
}
}
现在能不能直接把MyRunnableThread换成MyCallableThread。当然不行,thread的构造方法参数需要Runnable类型的数据模型,而MyCallableThread属于Callable类型的。
那么到底怎么使用Callable创建thread对象呢?
这里需要一个FutureTask,先认识该类,上源码:
发现:FutureTask其实可以充当了一个中间人的角色
/**
* 1. 创建Callable的实现类,并重写call()方法,该方法为线程执行体,并且该方法有返回值
*/
class MyCallableThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "执行了!");
return 200;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 2. 创建Callable的实例,并用FutureTask类来包装Callable对象
// 3. 创建FutureTask对象,需要一个Callable类型的参数
FutureTask task = new FutureTask<Integer>(new MyCallableThread());
// 4. 创建多线程,由于FutureTask的本质是Runnable的实现类,所以第一个参数可以直接使用task
new Thread(task, "threadName").start();
//new Thread(task, "threadName2").start();
/*while (!task.isDone()) {
System.out.println("wait...");
}*/
System.out.println(task.get());
System.out.println(Thread.currentThread().getName() + " over!");
}
}
FutureTask:未来的任务,用它就干一件事,异步调用。通常用它解决耗时任务,挂起堵塞问题。
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
FutureTask仅在call方法完成时才能get结果
;如果计算尚未完成,则阻塞 get 方法。
一旦计算完成,就不能再重新开始或取消计算。get方法获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
注意:
创建多个线程,会怎样?
运行结果:依然只有一个就是threadName。
如果想打印threadName2的结果,即不想复用之前的计算结果。怎么办?再创建一个FutureTask对象即可。
面试题:callable接口与runnable接口的区别?
相同点:都是接口,都可以编写多线程程序,都采用Thread.start()启动线程
不同点:
面试题:获得多线程的方法几种?
(1)继承thread类(2)runnable接口
如果只回答这两个你连被问到juc的机会都没有
正确答案如下:
传统的是继承thread类和实现runnable接口
java5以后又有实现callable接口和java的线程池获得
栈与队列简单回顾:
栈:先进后出,后进先出
队列:先进先出
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起
BlockingQueue即阻塞队列,是java.util.concurrent下的一个接口,因此不难理解,BlockingQueue是为了解决多线程中数据高效安全传输而提出的。从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种:
因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空队列进行出队列操作时,它将会被阻塞,除非有另一个线程进行了入队列操作。
阻塞队列主要用在生产者/消费者的场景,下面这幅图展示了一个线程生产、一个线程消费的场景:
为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
java.util.concurrent 包里的 BlockingQueue是一个接口,继承Queue接口,Queue接口继承 Collection。
BlockingQueue接口主要有以下7个实现类:
BlockingQueue接口有以下几个方法:
它的方法可以分成以下4类:
抛出异常
add正常执行返回true,element(不删除)和remove返回阻塞队列中的第一个元素
当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full
当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException
当阻塞队列空时,再调用element检查元素会抛出NoSuchElementException
特定值
插入方法,成功ture失败false
移除方法,成功返回出队列的元素,队列里没有就返回null
检查方法,成功返回队列中的元素,没有返回null
一直阻塞
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
当阻塞队列满时,再往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出
当阻塞队列空时,再从队列里take元素,队列会一直阻塞消费者线程直到队列可用
超时退出
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。
返回一个特定值以告知该操作是否成功(典型的是 true / false)。
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
// 第一组方法:add remove element
// System.out.println(queue.add("a"));
// System.out.println(queue.add("b"));
// System.out.println(queue.add("c"));
// // System.out.println(queue.add("d"));
// // System.out.println(queue.element());
// System.out.println(queue.remove());
// System.out.println(queue.remove());
// System.out.println(queue.remove());
// //System.out.println(queue.remove());
// //System.out.println(queue.element());
// 第二组:offer poll peek
// System.out.println(queue.offer("a"));
// System.out.println(queue.offer("b"));
// System.out.println(queue.offer("c"));
// System.out.println(queue.offer("d"));
// System.out.println(queue.peek());
// System.out.println(queue.poll());
// System.out.println(queue.poll());
// System.out.println(queue.poll());
// System.out.println(queue.poll());
// System.out.println(queue.peek());
// 第三组:put take
// queue.put("a");
// queue.put("b");
// queue.put("c");
// System.out.println(queue.take());
// queue.put("d");
// System.out.println(queue.take());
// System.out.println(queue.take());
// System.out.println(queue.take());
// 第四组:offer poll
System.out.println(queue.offer("a"));
System.out.println(queue.offer("b"));
System.out.println(queue.offer("c"));
System.out.println(queue.offer("d", 5, TimeUnit.SECONDS));
}
}
例子:
10年前单核CPU电脑,假的多线程,像马戏团小丑玩多个球,CPU需要来回切换。
现在是多核电脑,多个线程各自跑在独立的CPU上,不用切换效率高。
线程池的优势:线程复用;控制最大并发数;管理线程。
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,ExecutorService,ThreadPoolExecutor这几个类。
Executor接口是顶层接口,只有一个execute方法,过于简单。通常不使用它,而是使用ExecutorService接口:
那么问题来了,怎么创建一个连接池对象呢?通常使用Executors工具类
架构图可以看到Executors工具类,有没有联想到Collections,Arrays等。没错,可以用它快速创建线程池。
List list = Arrays.asList("");
ExecutorService threadPool = Executors.newCachedThreadPool();
直接编码演示:每种连接池的效果
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建单一线程的连接池
// ExecutorService threadPool = Executors.newSingleThreadExecutor();
// ExecutorService threadPool = Executors.newFixedThreadPool(3);
ExecutorService threadPool = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 5; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + "执行了业务逻辑");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
上述案例中的三个方法的本质都是ThreadPoolExecutor的实例化对象,只是具体参数值不同。
具体流程:
重要的事情说三遍:以下重要:以下重要:以下重要:
在创建了线程池后,线程池中的线程数为零。
当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
一般我们创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列,但这种模式下如果出现任务队列已满且线程池创建的线程数达到你设置的最大线程数时,这时就需要你指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池"超载"的情况。
ThreadPoolExecutor自带的拒绝策略如下:
以上内置的策略均实现了RejectedExecutionHandler接口,也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略
在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。而线程池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式,这一方面是由于jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建单一线程的连接池
// ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 创建固定数线程的连接池
// ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 可扩容连接池
// ExecutorService threadPool = Executors.newCachedThreadPool();
// 自定义连接池
ExecutorService threadPool = new ThreadPoolExecutor(2, 5,
2, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
//new ThreadPoolExecutor.AbortPolicy()
//new ThreadPoolExecutor.CallerRunsPolicy()
//new ThreadPoolExecutor.DiscardOldestPolicy()
//new ThreadPoolExecutor.DiscardPolicy()
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("自定义拒绝策略");
}
}
);
try {
for (int i = 0; i < 9; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行了业务逻辑");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
计算机运行架构图,如下:
由于cpu的运行程序速度远大于主存储的速度,所以会在主存RAM和CPU之间加多级高速缓存,缓存的速度接近cpu的运行速度,这样会大大提高计算机的运行速度。
JMM即为JAVA 内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从java 5开始的JSR-133发布后,已经成熟和完善起来。
安全性问题原因:
因为JMM的工作内存和主内存之间存在延迟
JMM规定了内存主要划分为主内存和工作内存两种。
主内存:保存了所有的变量。
共享变量:如果一个变量被多个线程使用,那么这个变量会在每个线程的工作内存中保有一个副本,这种变量就是共享变量。
工作内存:每个线程都有自己的工作内存,线程独享,保存了线程用到的变量副本(主内存共享变量的一份拷贝)。工作内存负责与线程交互,也负责与主内存交互。
此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的维度上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域
,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。
JMM对共享内存的操作做出了如下两条规定:
内存模型的三大特性:
volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。
因为JMM的工作内存和主内存之间存在延迟
,而且java会对一些指令进行重新排序。volatile和synchronized可以保证程序的有序性
,很多程序员只理解这两个关键字的执行互斥,而没有很好的理解到volatile和synchronized也能保证指令不进行重排序。
public class VolatileDemo {
private static Integer flag = 1;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是子线程工作内存flag的值:" + flag);
while(flag == 1){
}
System.out.println("子线程操作结束..." + flag);
}
}).start();
Thread.sleep(500);
flag = 2;
System.out.println("我是主线程工作内存flag的值:" + flag);
}
}
这是没有添加volatile关键字,打印效果如下:
子线程读取不到主线程修改后的flag值,陷入死循环程序无法结束。
接下来添加volatile关键字再试试:
打印结果如下:子线程可以读取的新值并结束子线程
public class VolatileOrderDemo {
//测试Volatile的有序性
static int a,b;
static int x,y;
public static void main(String[] args) throws InterruptedException {
int i=0;
while (true){
i++;
a=b=x=y=0;
Thread thread1 = new Thread(()->{
a=1;
x=b;
});
Thread thread2 = new Thread(()->{
b=1;
y=a;
});
thread1.start();
thread2.start();
thread1.join();//join的作用、阻塞 等线程执行完毕
thread2.join();
//线程之间的操作之间不影响,修改值后会同步到共享变量
System.out.println("第"+i+"次执行、x="+x+"、y="+y);
if (x==0&&y==0){
break;
}
}
}
}
正常情况下:
如果thread1先执行,xy的值是[0, 1] ;a=1 x=b {b:0} b=1 y=a {a:1}; x=0,y=1
如果thread2先执行是[1, 0] b = 1 y=a {a:0} a=1 x=b{b:1}
如果出现[0, 0],则说明进行了指令重排{因为JMM的工作内存和主内存之间存在延迟}。
thread1.b = main.b
thread1.a = main.a
thread1.x = main.x
thread1.y = main.y
a = 1; x = b;
1: thread1.a =1;
2: main.a=thread1.a
3: thread1.x = thread1.b;
4: main.x=thread1.x
a: thread2.b = main.b
b: thread2.a = main.a
c: thread2.x = main.x
d: thread2.y = main.y
b = 1; y = a;
5: thread2.b = 1;
6: main.b = thread2.b
7: thread2.y = thread2.a;
8: main.y=thread2.y
0 :0 4,8重排 1,b,2,3,5,6,7再 4,8
给a, b添加volatile关键字
其结果和上面不添加volatile关键字的一样,只不过执行次数不一样。
class DataOne{
private Integer number=0;
public Integer incur(){//public 后面添加synchronized即可保证原子性
return ++number;
}
}
public class VolatileAtomicDemo {
public static void main(String[] args) {
DataOne dataOne = new DataOne();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
//不具有原子性:不然最后会输出1000
System.out.println(dataOne.incur());
}).start();
}
}
}
1000个线程执行++number操作,如果++number操作具备原子性,最后的值应该是1000。说明++number不具备原子性。
接下来,给number添加volatile关键字:
class DataOne{
//测试Volatile的原子性
private volatile Integer number=0;
public Integer incur(){//public 后面添加synchronized即可保证原子性
return ++number;
}
}
public class VolatileAtomicDemo {
public static void main(String[] args) {
DataOne dataOne = new DataOne();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
//不具有原子性:不然最后会输出1000
System.out.println(dataOne.incur());
}).start();
}
}
}
说明volatile关键字不能保证原子性。
给incr方法添加同步锁试试:
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的
,因此不会将该变量上的操作与其他内存操作一起重排序。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
当一个变量定义为 volatile 之后,将具备两种特性:
所有的线程的可见性
。禁止指令重排序优化
。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障。volatile 性能:volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障
指令来保证处理器不发生乱序执行。
在常规的开发中,如果我们通过上述规则来分析一个并发程序是否安全,估计脑壳会很疼。因为更多时候,我们是分析一个并发程序是否安全,其实都依赖Happen-Before原则进行分析。Happen-Before被翻译成先行发生原则,意思就是当A操作先行发生于B操作,则在发生B操作的时候,操作A产生的影响能被B观察到,“影响”包括修改了内存中的共享变量的值、发送了消息、调用了方法等。
Happen-Before的规则有以下几条:
以上这些规则保障了happen-before的顺序,如果不符合以上规则,那么在多线程环境下就不能保证执行顺序等同于代码顺序。通过这些条件的判定,仍然很难判断一个线程是否能安全执行,线程安全多数依赖于工具类的安全性来保证。想提高自己对线程是否安全的判断能力,必然需要理解所使用的框架或者工具的实现,并积累线程安全的经验。
CAS:Compare and Swap。比较并交换的意思。CAS操作有3个基本参数:内存地址A,旧值B,新值C。它的作用是将指定内存地址A的内容与所给的旧值B相比,如果相等,则将其内容替换为指令中提供的新值C;如果不等,则更新失败。类似于修改登陆密码的过程。当用户输入的原密码和数据库中存储的原密码相同,才可以将原密码更新为新密码,否则就不能更新。
**CAS是解决多线程并发安全问题的一种乐观锁算法。**因为它在对共享变量更新之前,会先比较当前值是否与更新前的值一致,如果一致则更新,如果不一致则循环执行(称为自旋锁),直到当前值与更新前的值一致为止,才执行更新。
Unsafe类是CAS的核心类,提供硬件级别的原子操作(目前所有CPU基本都支持硬件级别的CAS操作)。
// 对象、对象的属性地址偏移量、预期值、修改值
要修改的变量是谁
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
Unsafe简单demo:
public class UnsafeDemo {
private int number = 0;
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
UnsafeDemo unsafeDemo = new UnsafeDemo();
System.out.println(unsafeDemo.number);// 修改前
unsafeDemo.compareAndSwap(0, 30);
System.out.println(unsafeDemo.number);// 修改后
}
public void compareAndSwap(int oldValue, int newValue){
try {
// 通过反射获取Unsafe类中的theUnsafe对象
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true); // 设置为可见
Unsafe unsafe = (Unsafe) theUnsafe.get(null); // 获取Unsafe对象
// 获取number的偏移量
long offset = unsafe.objectFieldOffset(UnsafeDemo.class.getDeclaredField("number"));
// cas操作
unsafe.compareAndSwapInt(this, offset, oldValue, newValue);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
在JUC下有个atomic包,有很多原子操作的包装类:
这里以AtomicInteger这个类来演示:
public class CasDemo {
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(1);
System.out.println("第一次更新:" + i.compareAndSet(1, 200));
System.out.println("第一次更新后i的值:" + i.get());
System.out.println("第二次更新:" + i.compareAndSet(1, 300));
System.out.println("第二次更新后i的值:" + i.get());
System.out.println("第三次更新:" + i.compareAndSet(200, 300));
System.out.println("第三次更新后i的值:" + i.get());
}
}
输出结果如下:
第一次更新:true
第一次更新后i的值:200
第二次更新:false
第二次更新后i的值:200
第三次更新:true
第三次更新后i的值:300
结果分析:
第一次更新:i的值(1)和预期值(1)相同,所以执行了更新操作,把i的值更新为200
第二次更新:i的值(200)和预期值(1)不同,所以不再执行更新操作
第三次更新:i的值(200)和预期值(1)相同,所以执行了更新操作,把i的值更新为300
还是改造之前的验证volatile关键字的案例如下:
class DataOne{
//测试Volatile的原子性
//private volatile Integer number=0;
private AtomicInteger number = new AtomicInteger(0);
public Integer incur(){//public 后面添加synchronized即可保证原子性
return number.incrementAndGet();
}
}
public class VolatileAtomicDemo {
public static void main(String[] args) {
DataOne dataOne = new DataOne();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
//不具有原子性:不然最后会输出1000
System.out.println(dataOne.incur());
}).start();
}
}
}
开销大:在并发量比较高的情况下,如果反复尝试更新某个变量,却又一直更新不成功,会给CPU带来较大的压力
ABA问题:当变量从A修改为B再修改回A时,变量值等于期望值A,但是无法判断是否修改,CAS操作在ABA修改后依然成功。
不能保证代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。
AbstractQueuedSynchronizer抽象队列同步器简称AQS,它是实现同步器的基础组件(框架),juc下面Lock(ReentrantLock、ReentrantReadWriteLock等)的实现以及一些并发工具类(Semaphore、CountDownLatch、CyclicBarrier等)就是通过AQS来实现的。具体用法是通过继承AQS实现其模板方法,然后将子类作为同步组件的内部类。
But
StampLock不是基于AQS实现的。 可以解决写饥饿问题
AQS框架结构如下:
AQS维护了一个volatile语义(支持多线程下的可见性)的共享资源变量state和一个FIFO(first-in-first-out)线程等待队列(多线程竞争state资源被阻塞时,会进入此队列)。
AQS将大部分的同步逻辑均已经实现好,继承的自定义同步器只需要实现state的获取(acquire)和释放(release)的逻辑代码就可以,主要包括下面方法:
也就是说:
通过AQS可以实现独占锁(只有一个线程可以获取到锁,如:ReentrantLock),也可以实现共享锁(多个线程都可以获取到锁Semaphore/CountDownLatch等)
jdk官方文档给出了使用案例:
把jdk文档中的案例copy到我们工程中:
就可以直接使用了:
public class AqsDemo {
public static void main(String[] args) throws InterruptedException {
DataThree dataThree = new DataThree();
CountDownLatch countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
new Thread(() -> {
dataThree.incr();
countDownLatch.countDown();
}, "").start();
}
countDownLatch.await();
System.out.println(dataThree.getNum());
}
}
class DataThree {
private volatile int num;
Mutex mutex = new Mutex();
public void incr(){
mutex.lock();
for (int i = 0; i < 1000; i++) {
num++;
}
mutex.unlock();
}
public int getNum(){
return num;
}
}
测试效果如下:
接下来就以ReetrantLock为例,说明AQS在锁底层的应用。
在ReentrantLock类中包含了3个AQS的实现类:
在ReetrantLock的源码中可以发现:
内部方法主要包括:
/**
* 自定义方法:为非公平锁的实现提供快捷路径
*/
abstract void lock();
/**
* 自定义通用方法,两个子类的tryAcquire方法都需要使用非公平的trylock方法
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 如果当前没有线程获取到锁
if (compareAndSetState(0, acquires)) { // 则CAS获取锁
setExclusiveOwnerThread(current); // 如果获取锁成功,把当前线程设置为有锁线程
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 如果当前线程已经拥有锁,则重入
int nextc = c + acquires; // 每重入一次stat累加acquires
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
/**
* 实现AQS的释放锁方法
*/
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 每释放一次stat就减releases
if (Thread.currentThread() != getExclusiveOwnerThread()) // 当前线程不是有锁线程抛异常
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // stat减为0则释放锁
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1)) // CAS把stat设置为1
setExclusiveOwnerThread(Thread.currentThread()); // 获取到锁
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires); // 使用了Sync抽象类的nonfairTryAcquire方法
}
}
acquire(1)方法是AQS自己实现的本质就是调用tryAcquire方法,如果tryAcquire获取到锁并无法进入等待队列则中止线程。
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // 从线程有序等待队列中获取等待
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 可重入
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
hasQueuedPredecessors具体实现如下:
synchronized:偏向锁(偏向第一个线程,效率最高) —> 如果有线程竞争升级为轻量级锁(自旋锁) —> 自旋10次升级为重量级锁(悲观锁)