JUC
文章目录
一、线程常用语句
//获取cpu的核数
Runtime.getRuntime().availableProcessors();
并发编程的本质:充分利用CPU的资源。
线程休眠方式(不会直接将线程sleep,一般都采用TimeUtil类)
//休眠1天
TimeUnit.DAYS.sleep(1);
//休眠2小时
TimeUnit.HOURS.sleep(2);
二、线程创建方式(松耦合)
一开始学习的线程创建都是通过,类实现Runnable接口或者继承Thread类等。
class MyThread implement Runnable{
...
}
但是这种方法,实现类只能用于当作线程类,耦合度高,因此不能采用这种直接继承或者实现类和接口的方式。
把线程类创建成一个普通的类,只有属性和方法。
class MyThread{
...
}
然后在调用线程类的时候,用匿名内部类的方式,来将线程类和线程接口绑定。
class Demo{
MyThread myThread=new MyThread();
new Thread(new Runnable(){
@Override
public void run(){
myThread.方法;
}
}).start;
}
采用上述方式,可以降低耦合。但是上面的方式,写法太复杂,因此采用Lambda表达式简化代码。
class Demo{
MyThread myThread=new MyThread();
new Thread(()->{
myThread.方法;
}).start;
}
三、线程通信–生产者消费者模式
1、synchreonized 方法锁方式
public class PCTest3 {
public static void main(String[] args) {
Products products=new Products();
//降低耦合,资源类在main函数中,以匿名内部类的形式实现Runnable接口,其本身在定义的时候不实现Runnable接口。
Thread t1=new Thread(()->{
for (int i = 0; i < 20; i++) {
products.product();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 20; i++) {
products.consume();
}
});
t1.start();
t2.start();
}
}
//资源类
class Products{
int count=0;
//生产方法
public synchronized void product(){
if (count!=0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println("生产者生产了--->" + count);
this.notifyAll();
}
//消费方法
public synchronized void consume(){
if (count==0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
System.out.println("消费者消费了--->" + count);
this.notifyAll();
}
}
虚假唤醒
以上这种方式,在只有两个线程(生产和消费)时不会出错,但是当多于两个线程时,可能会出现虚假唤醒。
public class PCTest3 {
public static void main(String[] args) {
Products products=new Products();
//降低耦合,资源类在main函数中,以匿名内部类的形式实现Runnable接口,其本身在定义的时候不实现Runnable接口。
Thread t1=new Thread(()->{
for (int i = 0; i < 20; i++) {
products.product();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 20; i++) {
products.consume();
}
});
Thread t3=new Thread(()->{
for (int i = 0; i < 20; i++) {
products.product();
}
});
Thread t4=new Thread(()->{
for (int i = 0; i < 20; i++) {
products.consume();
}
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}
上面是,四个线程的情况,两个消费,两个生产。
运行多次后,终于发现了其中一次出现错误,生产者生产了2个3个这些都是错误的。正确的应该是生产者生产一个,消费者消费一个。
这就是虚假唤醒导致的。
虚假唤醒的原因是,同步方法中的if语句,只执行一次。
因此当生产线程1生产了一件产品之后,此时count变为1,暂停该线程,资源类解锁,然后唤醒其他三条线程,如果生产线程2获得了锁,方法是从生产线程2的wait方法之后开始运行的,因为if语句只判断一次,所以wait之后不会在进行一次判断,会直接进行count++运算,count变成了2,这是不允许出现的,此时生产线程2相当于被虚假唤醒。
如果是消费线程,生产线程生产了一个产品,唤醒了其他三条线程,如果消费线程1获得了锁,消费线程1把产品取走了,count–,此时count的数量为0,然后唤醒其他线程,如果此时消费线程2获得了锁,他也是从wait一会开始执行,不会再判断count是不是=0,因此又会执行一次count–,此时count的数目为-1。因此消费线程2被虚假唤醒。
避免上面错误的方式很简单,把if改成while就行了,这样每当线程被唤醒的时候,即使是从wait方法以后开始执行依然会在返回去判断一次条件如果不满足,会再一次wait此线程,就不会出现上面的问题了。
package com.ouc.a302;
import java.util.function.Consumer;
public class PCTest3 {
public static void main(String[] args) {
Products products=new Products();
//降低耦合,资源类在main函数中,以匿名内部类的形式实现Runnable接口,其本身在定义的时候不实现Runnable接口。
Thread t1=new Thread(()->{
for (int i = 0; i < 100; i++) {
products.product();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 100; i++) {
products.consume();
}
});
Thread t3=new Thread(()->{
for (int i = 0; i < 100; i++) {
products.product();
}
});
Thread t4=new Thread(()->{
for (int i = 0; i < 100; i++) {
products.consume();
}
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//资源类
class Products{
int count=0;
//生产方法
public synchronized void product(){
while (count!=0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println("生产者生产了--->" + count);
this.notifyAll();
}
//消费方法
public synchronized void consume(){
while (count==0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
System.out.println("消费者消费了--->" + count);
this.notifyAll();
}
}
2、Lock锁方式(JUC版)
package com.ouc.a302;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
public class PCTest3 {
public static void main(String[] args) {
Products products = new Products();
//降低耦合,资源类在main函数中,以匿名内部类的形式实现Runnable接口,其本身在定义的时候不实现Runnable接口。
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
products.product();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
products.consume();
}
});
Thread t3 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
products.product();
}
});
Thread t4 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
products.consume();
}
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//资源类
class Products {
int count = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//生产方法
public void product() {
lock.lock();
try {
while (count != 0) {
condition.await();//相当于wait
}
count++;
System.out.println("生产者生产了--->" + count);
condition.signalAll();//相当于notifyAll
} catch (InterruptedException e) {
lock.unlock();
}
}
//消费方法
public void consume() {
lock.lock();
try {
while (count == 0) {
condition.await();
}
count--;
System.out.println("消费者消费了--->" + count);
condition.signalAll();
} catch (InterruptedException e) {
lock.unlock();
}
}
}
Condition 可以精准的通知和唤醒线程。
package com.ouc.a302;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
public class PCTest3 {
public static void main(String[] args) {
Control control = new Control();
//降低耦合,资源类在main函数中,以匿名内部类的形式实现Runnable接口,其本身在定义的时候不实现Runnable接口。
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
control.testA();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
control.testB();
}
});
Thread t3 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
control.testC();
}
});
t1.start();
t2.start();
t3.start();
}
}
//资源类
class Control {
Lock lock = new ReentrantLock();
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();
int flag = 1;
//生产方法
public void testA() {
lock.lock();
try {
while (flag != 1) {
conditionA.await();
}
System.out.println("A has run");
//精确唤醒B
flag = 2;
conditionB.signal();
} catch (InterruptedException e) {
lock.unlock();
}
}
public void testB() {
lock.lock();
try {
while (flag != 2) {
conditionB.await();
}
System.out.println("B has run");
//精确唤醒C
flag = 3;
conditionC.signal();
} catch (InterruptedException e) {
lock.unlock();
}
}
public void testC() {
lock.lock();
try {
while (flag != 3) {
conditionC.await();
}
System.out.println("C has run");
//精确唤醒C
flag = 1;
conditionA.signal();
} catch (InterruptedException e) {
lock.unlock();
}
}
}
读写锁
只锁写,不锁读。
独占锁(写锁)一次只能被一个线程调用
共享锁(读锁)一次可以同时被多条线程调用
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
public static void main(String[] args) {
final MyCacheReadWriteLock myCacheReadWriteLock=new MyCacheReadWriteLock();
for (int i = 0; i < 5; i++) {
final String temp=i+"";
new Thread(()->{
myCacheReadWriteLock.put(temp,temp);
},temp).start();
}
for (int i = 0; i < 5; i++) {
final String temp=i+"";
new Thread(()->{
myCacheReadWriteLock.get(temp);
},temp).start();
}
}
}
class MyCacheReadWriteLock{
private volatile Map<String,Object> map=new HashMap<>();
//读写锁
private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
public void get(String key){
System.out.println(Thread.currentThread().getName()+"获取了--->"+key);
map.get(key);
System.out.println(Thread.currentThread().getName()+"获取"+key+"成功");
}
public void put(String key, Object o){
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"录入了--->"+key);
map.put(key,o);
System.out.println(Thread.currentThread().getName()+"录入取"+key+"成功");
} catch (Exception e) {
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
}
}
四、八锁问题
本例子,以两个输出方法printA和printB为例。
1、标准情况下,是先输出A还是先输出B?
public class EightLockQuestion {
public static void main(String[] args) {
Demo demo=new Demo();
new Thread(()->{
demo.printA();
},"A").start();
new Thread(()->{
demo.printB();
},"B").start();
}
}
class Demo{
//打印A
public synchronized void printA(){
System.out.println("printResult=A");
}
//打印B
public synchronized void printB(){
System.out.println("printResult=B");
}
}
先输出A再输出B,原因是,synchronized锁是锁住了调用同步方法的对象,该例子中,只有一个demo对象,demo对象调用了printA和printB,因此这两个方法共用一把锁,printA在前面调用,因此他先获得了锁,printB只有等待A释放锁之后,才能调用,因此先输出A后输出B。
2、如果在printA中定义一个延迟,则先输出A还是先输出B?
public class EightLockQuestion {
public static void main(String[] args) {
Demo demo=new Demo();
new Thread(()->{
demo.printA();
},"A").start();
new Thread(()->{
demo.printB();
},"B").start();
}
}
class Demo{
//打印A
public synchronized void printA(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("printResult=A");
}
//打印B
public synchronized void printB(){
System.out.println("printResult=B");
}
}
依旧是先等4秒输出A再输出B,和1的原因一样,A先被demo调用了,因此他先获得了锁,B只能等待A释放锁才能执行,因此即使是A睡眠了4秒,B依旧不能执行,只能等A完全执行完了才能执行。
3、带同步锁的同步方法和不带同步锁的普通方法,被同一个对象调用,是先输出A还是B
public class EightLockQuestion {
public static void main(String[] args) {
Demo demo=new Demo();
new Thread(()->{
demo.printA();
},"A").start();
new Thread(()->{
demo.printB();
},"B").start();
}
}
class Demo{
//打印A
public synchronized void printA(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("printResult=A");
}
//打印B
public void printB(){
System.out.println("printResult=B");
}
}
结果是,先输出B再输出A,原因是,printB是一个普通方法,不受锁的控制,程序一运行,不用管锁就可以直接运行,而printA还有一个4秒的延迟,因此先输出B再输出A。
4、带同步锁的同步方法,被2个对象调用,是先输出A还是B
public class EightLockQuestion {
public static void main(String[] args) {
Demo demo1=new Demo();
Demo demo2=new Demo();
new Thread(()->{
demo1.printA();
},"A").start();
new Thread(()->{
demo2.printB();
},"B").start();
}
}
class Demo{
//打印A
public synchronized void printA(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("printResult=A");
}
//打印B
public synchronized void printB(){
System.out.println("printResult=B");
}
}
先输出B再输出A,原因是,synchronized锁只锁住了调用他的对象,当有两个对象调用它的时候,就有两把锁,printA和printB分别有两把锁,因此他们不需要等待就可以直接运行,printA有一个两秒的延迟,因此先输出B再输出A。
5、2个静态同步方法,被一个对象调用,是先输出A还是先输出B
public class EightLockQuestion {
public static void main(String[] args) {
Demo demo=new Demo();
new Thread(()->{
demo.printA();
},"A").start();
new Thread(()->{
demo.printB();
},"B").start();
}
}
class Demo{
//打印A
public static synchronized void printA(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("printResult=A");
}
//打印B
public static synchronized void printB(){
System.out.println("printResult=B");
}
}
是输出A再输出B,原因是,static代表静态方法,静态方法在类被创建的时候就已经生效了,静态方法的synchronized锁锁的是Demo类模板(即Demo.class对象),一个类只有一个该对象,因此printA和printB用一把锁,printA先被调用,因此他先获得锁,等他输出完之后,printB才能获得锁。因此先输出A再输出B。
6、2个静态同步方法,被2个对象调用,是先输出A还是先输出B
public class EightLockQuestion {
public static void main(String[] args) {
Demo demo1=new Demo();
Demo demo2=new Demo();
new Thread(()->{
demo1.printA();
},"A").start();
new Thread(()->{
demo2.printB();
},"B").start();
}
}
class Demo{
//打印A
public static synchronized void printA(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("printResult=A");
}
//打印B
public static synchronized void printB(){
System.out.println("printResult=B");
}
}
先输出A再输出B,因为static静态方法是锁住了类模板,一个类只有这一个锁,因此不管定义多少个对象,都只有这一把锁,printA先定义,因此A先获得锁,输出A,然后在输出B。
7、普通同步方法和静态同步方法,被一个对象调用。
public class EightLockQuestion {
public static void main(String[] args) {
Demo demo=new Demo();
new Thread(()->{
demo.printA();
},"A").start();
new Thread(()->{
demo.printB();
},"B").start();
}
}
class Demo{
//打印A
public static synchronized void printA(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("printResult=A");
}
//打印B
public synchronized void printB(){
System.out.println("printResult=B");
}
}
先输出B在输出A,因为,静态同步方法锁住的是类模板Demo.class对象,而普通同步方法锁住的是Demo类,因此该例子有两把锁,printA和printB不用等待,可以直接运行,printA有两秒延时,因此先输出B。
8、普通同步方法和静态同步方法,被2个对象调用。
public class EightLockQuestion {
public static void main(String[] args) {
Demo demo1=new Demo();
Demo demo2=new Demo();
new Thread(()->{
demo1.printA();
},"A").start();
new Thread(()->{
demo2.printB();
},"B").start();
}
}
class Demo{
//打印A
public static synchronized void printA(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("printResult=A");
}
//打印B
public synchronized void printB(){
System.out.println("printResult=B");
}
}
先输出B在输出A,这个和上面一样,也是有两把锁,printA隶属于Demo.class的锁,而printB隶属于Demo2对象的锁,因此printA由于有两秒延时,所以先输出了B。
五、集合不安全
1.List不安全
list集合在单线程下是安全的,但是多线程不安全
public class ListNoSafe {
public static void main(String[] args) {
List list=new ArrayList();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println("list = " + list);
},String.valueOf(i)).start();
}
}
}
java.util.ConcurrentModificationException并发修改异常。
解决方法有三个:
1、List list=new Vector<>();
2、List list =Collections.synchronizedList(new ArrayList<>());
3、juc推荐方法—> List list=new CopyOnWriteArrayList();
CopyOnWriteArrayList与Vector相比的优点在于,
CopyOnWriteArrayList采用Lock锁,Vector采用synchronized锁,因此CopyOnWriteArrayList效率更高更灵活
CopyOnWriteArrayList底层代码
Vector底层代码
2.set不安全
采用
Set set=new CopyOnWriteArraySet();
3.Map不安全
Map map=new ConcurrentHashMap();
六、Callable
特点:
有返回值
可以抛出异常
方法不同:call()/run()
Callable如何让Thread使用----通过java api文档寻找方法
点进去之后发现了Callable
说明Callable就是通过Runnable的FutureTask让Thread获取的。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//new Thread(new FutureTask<String>(new MyDemoThread()),"CALLABLE").start();
FutureTask futureTask=new FutureTask<String>(new MyDemoThread());
new Thread(futureTask,"CALLABLE").start();
String string = (String)futureTask.get();
System.out.println("string = " + string);
}
}
class MyDemoThread implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("call方法");
return "call";
}
}
String string = (String)futureTask.get();可能导致线程阻塞,因为他会一直等,直到获得了值才能运行,一般把他放到最后,可以采用异步的方式来解决。
七、常用辅助类
1、CountDownLatch
减法计数器
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch=new CountDownLatch(5);
for (int i = 0; i <5; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() +"->"+"finished");
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("All Thread closed");
}
}
2、CyclicBarrier
加法计数器
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier=new CyclicBarrier(5,()->{
System.out.println(" All the thread created success");
});
for (int i = 0; i <5 ; i++) {
//必须这么写否则传不了值
final int index=i;
new Thread(()->{
System.out.println("number" + index+"Thread has been created successfully");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
3、Semaphore
信号量
public class SemaphoreTest {
public static void main(String[] args) {
Semaphore semaphore=new Semaphore(3);
for (int i = 0; i <5 ; i++) {
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"-->block Running");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"-->block leave");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
}).start();
}
}
}
上面的例子 Semaphore semaphore=new Semaphore(3);设置了线程最多运行三条,三条运行完之后释放资源,剩下的才能运行。
semaphore.acquire(); 获取线程,如果满了会等待
semaphore.release(); 释放线程
八、队列
1、阻塞队列
阻塞:
写入的是如果队列满了,则会阻塞等待;
读取的时候如果队列为空,则会阻塞等待。
队列:先进先出原则
什么情况下使用阻塞队列:
多线程并发处理/线程池
四组API
方式 | 抛出异常 | 不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer(“a”,时间,单位) |
移除 | remove | poll | take | poll(“a”,时间,单位) |
判断队列首 | element | peek |
1、抛出异常
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
for (int i = 0; i < 4; i++) {
System.out.println("存:i = " + i);
blockingQueue.add(i + "");
}
for (int i = 0; i < 3; i++) {
System.out.println("取:i = " + i);
blockingQueue.remove();
}
}
2、不会抛出异常
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
for (int i = 0; i < 4; i++) {
System.out.println("存:i = " + i);
blockingQueue.offer(i + "");
}
for (int i = 0; i < 3; i++) {
System.out.println("取:i = " + i);
blockingQueue.poll();
}
}
查看队首。不跳出异常用peek,跳出异常用element
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
for (int i = 0; i < 4; i++) {
System.out.println("存:i = " + i);
blockingQueue.offer(i + "");
}
System.out.println(blockingQueue.peek());
for (int i = 0; i < 3; i++) {
System.out.println("取:i = " + i);
blockingQueue.poll();
}
}
3、阻塞等待
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
for (int i = 0; i < 4; i++) {
System.out.println("存:i = " + i);
blockingQueue.put(i + "");
}
System.out.println(blockingQueue.peek());
for (int i = 0; i < 3; i++) {
System.out.println("取:i = " + i);
blockingQueue.take();
}
}
超出了范围会一直阻塞等待
4、超时等待
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
for (int i = 0; i < 4; i++) {
System.out.println("存:i = " + i);
blockingQueue.offer(i+"",2, TimeUnit.SECONDS);
//阻塞两秒后,如果还是没有位置,会自动往下进行
}
System.out.println(blockingQueue.peek());
for (int i = 0; i < 5; i++) {
System.out.println("取:i = " + i);
blockingQueue.poll(2,TimeUnit.SECONDS);
}
}
2、同步队列
SynchronousQueue
没有容量,必须等待取出来之后,才能再往里面放一个元素。
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "put--->" + 1);
blockingQueue.put("1");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "put--->" + 2);
blockingQueue.put("2");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "put--->" + 3);
blockingQueue.put("3");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "T1").start();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "-->"+blockingQueue.take());
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "-->"+blockingQueue.take());
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "-->"+blockingQueue.take());
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "T2").start();
}
九、线程池
线程池的好处
1.降低资源消耗
2.提高响应速度
3.方便管理
三大方法、七大参数、四种拒绝策略
1、Executors 工具类—> 三大方法
//单个线程 Executors.newSingleThreadExecutor(); //创建一个固定的线程池的大小 Executors.newFixedThreadPool(5); //可伸缩的线程池 Executors.newCachedThreadPool();
(1)单个线程Executors.newSingleThreadExecutor();
public class PoolDemo {
public static void main(String[] args) {
// //单个线程
// Executors.newSingleThreadExecutor();
// //创建一个固定的线程池的大小
// Executors.newFixedThreadPool(5);
// //可伸缩的线程池
// Executors.newCachedThreadPool();
ExecutorService executorService=Executors.newSingleThreadExecutor();
try {
for (int i = 0; i < 5; i++) {
final String index=i+"";
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"->"+index);
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
}
(2)创建一个固定的线程池的大小
Executors.newFixedThreadPool(5);
public class PoolDemo {
public static void main(String[] args) {
// //单个线程
// Executors.newSingleThreadExecutor();
// //创建一个固定的线程池的大小
// Executors.newFixedThreadPool(5);
// //可伸缩的线程池
// Executors.newCachedThreadPool();
ExecutorService executorService=Executors.newFixedThreadPool(5);
try {
for (int i = 0; i < 10; i++) {
final String index=i+"";
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"->"+index);
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
}
(3)可伸缩的线程池
Executors.newCachedThreadPool();
public class PoolDemo {
public static void main(String[] args) {
// //单个线程
// Executors.newSingleThreadExecutor();
// //创建一个固定的线程池的大小
// Executors.newFixedThreadPool(5);
// //可伸缩的线程池
// Executors.newCachedThreadPool();
ExecutorService executorService=Executors.newCachedThreadPool();
try {
for (int i = 0; i < 10; i++) {
final String index=i+"";
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"->"+index);
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
}
2、七大参数
源码分析
(1)newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
(2)newSingleThreadExecutor(ThreadFactory threadFactory)
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
(3)newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
发现三种方法底层都是调用了ThreadPoolExecutor
(4)ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,//核心线程池大小
int maximumPoolSize,//最大核心线程池大小
long keepAliveTime,//超时了没有人调用就会释放
TimeUnit unit,//超市单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//线程工厂,创建线程的,一般不用动
RejectedExecutionHandler handler)//拒绝策咯 {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
int corePoolSize,//核心线程池大小
int maximumPoolSize,//最大核心线程池大小
long keepAliveTime,//超时了没有人调用就会释放
TimeUnit unit,//超市单位
BlockingQueue workQueue,//阻塞队列
ThreadFactory threadFactory,//线程工厂,创建线程的,一般不用动
RejectedExecutionHandler handler)//拒绝策咯
线程数量小于corePoolSize会不断创建线程,加入线程池。
当线程数量大于corePoolSIze时多出来的线程会进入阻塞队列BlockingQueue workQueue;
注:
线程池不要使用Executors来创建,而要使用ThreadPoolExecutor的方式。Executors有两个弊端,
1)FixedThreadPool和singleThreadPool: 核心线程池大小为1,如果线程很多会无限存入阻塞队列,阻塞队列采用LinkedBlockingQueue,队列长度Integer.MAX_VALUE,因此阻塞队列可能会堆积大量的请求,导致OOM(请求溢出);(out of memory)
2)CachedThreadPool和ScheduledThreadPool: 核心线程池大小为Integer.MAX——Value,线程会无限创建,可能会创建大量的线程,导致OOM(请求溢出)
原理图
public class PoolDemo {
public static void main(String[] args) {
ExecutorService executorService= new ThreadPoolExecutor(
// int corePoolSize,
// int maximumPoolSize,
// long keepAliveTime,
// TimeUnit unit,
// BlockingQueue<Runnable> workQueue,
// ThreadFactory threadFactory,
// RejectedExecutionHandler handler
3,//开启的核心窗口数
5,//总窗口数
3,//长时间不操作,自动关闭窗口的时间
TimeUnit.SECONDS,//时间单位
new LinkedBlockingDeque<>(3),//候选区的容量
Executors.defaultThreadFactory(),//工厂类,一边不变
new ThreadPoolExecutor.AbortPolicy()//拒绝策略,如果窗口满了和候选区都满了,就要采用拒绝策略,拒绝后面的客户来办业务。
);
try {
for (int i = 0; i < 8; i++) {
final String index=i+"";
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"->"+index);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
}
6个线程
上图可以发现,只有线程1、2、3在跑,
7个线程
上图发现线程4也加入了。
8个线程
5个线程都加入了。
9个线程
超出最大数量,抛出异常了。最大数量=max+阻塞队列
3、4个拒绝策略
AbortPolicy() //超了,拒绝,然后抛出异常 CallerRunsPolicy()//哪来的去哪里,让main线程去执行(即哪个线程来的,让哪个线程执行,main线程来的就main执行) DiscardPolicy()//队列满了,丢掉任务,不会报错 DiscardOldestPolicy()//队列满了,会尝试和最早的竞争,如果最早的快执行完了,他就顶上,如果没有那就丢掉任务。也不会抛出异常。
4、总结
最大线程到底该如何让定义
1、cpu密集型---------几核就定义为几 Runtime.getRuntime().avaliableProcessors()------>动态获取cpu核数
2、io密集型-------------判断,程序中十分耗io的线程,大于这个数,一般设置成2倍
十、四大函数式接口(重点)
函数式接口:只有一个方法的接口
1、Function 函数型接口
import java.util.function.Function;
public class InterfaceDemo {
public static void main(String[] args) {
//方式一、内部类写法
Function function=new Function<String,String>() {
@Override
public String apply(String string) {
return string;
}
};
String apply = (String)function.apply("123");
System.out.println(apply);
//方式二、lambda表达式写法
Function<Integer,Integer> function1=(index)->{
return index+1;
};
System.out.println("index=" + function1.apply(5));
}
}
2、Predicate 断定型接口
有一个输入参数,返回值只能是布尔值。
public class InterfaceDemo {
public static void main(String[] args) {
//判断str是否为空
Predicate<String> predicate=new Predicate<String>() {
@Override
public boolean test(String str) {
return str.equals("");
}
};
System.out.println(predicate.test(""));
//判断value是否为0
Predicate<Integer> predicate1=value->{
return value==0;
};
System.out.println(predicate1.test(1));
}
}
3、Consumer 消费型接口
只有输入没有返回值
public class InterfaceDemo {
public static void main(String[] args) {
Consumer<String> consumer=new Consumer<String>() {
@Override
public void accept(String o) {
System.out.println("o = " + o);
}
};
consumer.accept("consumer");
Consumer<String> consumer1=(str)->{
System.out.println("str = " + str);
};
consumer1.accept("consumer1");
}
}
4、Supplier 供给型接口
只有返回值,没有参数
public class InterfaceDemo {
public static void main(String[] args) {
Supplier<String> supplier=new Supplier<String>() {
@Override
public String get() {
return "Supplier";
}
};
System.out.println(supplier.get());
Supplier<String> supplier1=()->{
return "Supplier1";
};
System.out.println(supplier1.get());
}
}
十一、Stream流式计算
步骤:
1.对象.stream()------>把对象转换成Stream对象。
2.利用filter方法根据条件过滤
filter的参数是一个Predicate接口,
import java.util.Arrays;
import java.util.List;
public class StreamDemo {
public static void main(String[] args) {
User user1=new User(1,"green",true);
User user2=new User(2,"yello",false);
User user3=new User(3,"red",false);
User user4=new User(4,"pink",false);
User user5=new User(5,"blue",true);
List<User> users = Arrays.asList(user1, user2, user3, user4, user5);
users.stream().filter((user)->{
return user.getId()%2==0;
}).map((u)->{
String s = u.getName().toUpperCase();
u.setName(s);
return u;
}).sorted((u1,u2)->{
return u1.getName().compareTo(u2.getName());
}).limit(1).forEach(System.out::println);
}
}
class User{
private int id;
private String name;
private boolean gender;
public User(int id, String name, boolean gender) {
this.id = id;
this.name = name;
this.gender = gender;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isGender() {
return gender;
}
public void setGender(boolean gender) {
this.gender = gender;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", gender=" + gender +
'}';
}
}
十二、ForkJoin 分支合并
用于并行执行任务,提高效率。
ForkJoin特点:工作窃取
操作双端队列。
递归
public class ForkJoinDemo extends RecursiveTask<Long> {
private final Long num=10000L;
private Long start;
private Long end;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if ((end-start)<num){
long sum=0;
for (long i = start; i < end; i++) {
sum++;
}
return sum;
}else {
Long mid=(start+end)/2;
ForkJoinDemo forkJoinDemo1=new ForkJoinDemo(start,mid);
//把线程压入队列
forkJoinDemo1.fork();
ForkJoinDemo forkJoinDemo2=new ForkJoinDemo(mid+1,end);
forkJoinDemo2.fork();
return forkJoinDemo1.join()+forkJoinDemo2.join();
}
}
}
class Test{
public static void main(String[] args) throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool=new ForkJoinPool();
ForkJoinDemo forkJoinDemo=new ForkJoinDemo(0L,100000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinDemo);
Long aLong = submit.get();
long end = System.currentTimeMillis();
System.out.println(aLong+(end-start));
}
}
//方法2,流计算--->特别快
class Test{
public static void main(String[] args) throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
long reduce = LongStream.rangeClosed(0, 10000L).reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println(reduce+"---->"+(end-start));
}
}
十三、异步回调
public class FutureDemo {
public static void main(String[] args) {
FutureDemo.runAsync();
}
//没有返回值的runAsync异步回调
public static void runAsync(){
CompletableFuture<Void> completableFuture=CompletableFuture.runAsync(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
});
System.out.println("codeBlock01");
try {
completableFuture.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
//有返回值的supplyAsync异步回调
public static void supplyAsync() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> completableFuture=CompletableFuture.supplyAsync(()->{
return 1024;
});
//异常处理
System.out.println(completableFuture.whenComplete((t, u) -> {
System.out.println("t = " + t);//正常的返回结果
System.out.println("u = " + u);//错误信息
}).exceptionally((e) -> {
e.printStackTrace();
e.getMessage();
return 233;//可以获取到错误的返回结果
}).get());
}
}
十四、JMM&volatile
这一部分主要针对下面三点进行研究。
volatile是java虚拟机提供轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
1、JMM
什么是JMM?
JMM:java内存模型,不存在的东西。是概念约定
关于JMM的一些同步约定
1、线程解锁前,必须共享变量 立刻刷回主存
2、线程加锁前,必须读取主存中的最新值到工作内存中。
3、加锁和解锁是同一把锁。
8种操作
上面 是六种操作方式,还有两种是lock和unlock。
现在存在一个问题,就是当主存中flag=true,他把flag=true传到线程A和线程B中,此时如果线程B把flag改成false,那么主存中的flag也会被改为flase,但是线程A仍然是true,这就出现了矛盾。下面有一个例子可以证明这一矛盾。
public class JMMDemo {
private static int num=0;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (num==0){
}
}).start();
TimeUnit.SECONDS.sleep(4);
num=1;
System.out.println("num = " + num);
}
}
按照常规来分析,当num=1的时候,while循环应该终止,但是实际上却发现,并没有终止,而是进入了死循环。
main线程相当于线程B,main线程把num变成1,主存中num也变成1,但是thread线程里面的num仍然是0,因此会一直循环。
要解决这个问题就要采用volatile的第一个特性 保证可见性
2、volatile
public class JMMDemo {
//加上volatile
private volatile static int num=0;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (num==0){
}
}).start();
TimeUnit.SECONDS.sleep(4);
num=1;
System.out.println("num = " + num);
}
}
(1)保存可见性
保证可见性
就是线程对于主存的变化是可见的。即主存发生变化,线程也会立刻知道,并和主存保持一致。
加上volatile,就代表num属性一旦主存中改变线程中的num也会改变,就不会出现main线程把num改为1,而线程1还不知道,num仍然为0的现象。
(2)不保证原子性
原子性,线程在执行过程中不能被打扰也不能被分割。要么同时成功要么同时失败。
下面举一个例子来证明这一点。
public class automicDemo {
private volatile static int num=0;
public static void add(){
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 1000; i1++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"->"+num);
}
}
20个线程每个循环1000次,每次num加1,最终结果应该是20000.但是发现结果和预想的不一样。
看底层字节码
第一步,target里面找到当前类的字节码文件
第二步,进入字节码文件所在文件目录
第三步,在文件路径处输入cmd进入命令行。
第四步,输入javap -c 类名(带后缀)
以上就是反编译查看字节码的步骤。通过字节码可知,线程1调用add时,线程2也可能调用,这时候线程1使num+1,线程2也使num+1,当俩个都给num赋值的时候,相当于只加了1次1.因此最后结果过小于20000.
这就是不保证原子性。
那么应该如何解决呢?
------采用原子类(java.util.concurrent.automic包)
public class automicDemo {
//原子类--整型子类
private volatile static AtomicInteger num=new AtomicInteger();
public static void add(){
num.getAndIncrement();//相当于num++ 这个方法很高级,通过unsafe类直接操作内存,效率很高。
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 1000; i1++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"->"+num);
}
}
(3)禁止指令重排
指令重排,是指你写的程序,计算机并不是按照你写的顺序那样去执行。
源代码在执行过程中会出现三次重排,
源代码–》编译器优化的重排–》指令并行也可能会重排–》内存系统也会重排–》执行
指令重排会考虑数据之间的依赖性,不会让后面依赖的参数,排到需要他的参数之后。
这种指令重排有小概率让系统数据出现错误。
volatile可以避免指令重排
主要原理就是采用了内存屏障。
十五、JUC的各种锁
1、公平锁&非公平锁
公平锁:非常公平,不能插队,必须先来后到
非公平锁:非常不公平,可以插队(默认都是非公平锁)
//定义lock锁的时候采用无参的new ReentrantLock()创建lock锁,默认是非公平锁
Lock lock=new ReentrantLock();
//如果才有有参的,且传参true,则创建公平锁
Lock lock=new ReentrantLock(true);
2、可重入锁
是递归锁
就好比一个房子有个门上面有锁,房子里面卧室也有锁,当打开了房子的锁,我是的锁也默认打开了。
public class LockDemo implements Runnable{
public synchronized void sms(){
System.out.println(Thread.currentThread().getName()+"->"+"sms");
call();
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName()+"->"+"call");
}
@Override
public void run() {
System.out.println("true = " + true);
sms();
}
public static void main(String[] args) {
LockDemo lockDemo=new LockDemo();
new Thread(()->{
lockDemo.sms();
},"a").start();
new Thread(()->{
lockDemo.sms();
},"b").start();
}
}
试了好多次,都是现a执行完,b才能执行。这就是可重入锁。
如果是lock锁,可重入锁是两把锁,但也是第一把锁开了,里面的小锁也开了。但是锁一定要配对,lock()了几把锁,最后就要unlock()几把锁。
3、自旋锁
在cas里面出现过,具体看上一个博客。
public class LockDemo{
private AtomicReference<Thread> atomicReference=new AtomicReference<>();
public void mylock(){
Thread thread=Thread.currentThread();
while (!atomicReference.compareAndSet(null,thread)){
}
System.out.println(Thread.currentThread().getName()+"->"+"mylock");
}
public void unlock(){
Thread thread=Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"->"+"unlock");
}
public static void main(String[] args) {
LockDemo lockDemo=new LockDemo();
new Thread(()->{
lockDemo.mylock();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lockDemo.unlock();
}
},"t1").start();
new Thread(()->{
lockDemo.mylock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lockDemo.unlock();
}
},"t2").start();
}
}
t1线程进入锁之后,要等2秒钟,此时她线程进入自旋,知道t1锁释放,t2锁才能接触自旋,运行后面的代码。
4、死锁排查
public class LockDemo{
public static void main(String[] args) {
myThread myThread1=new myThread("a","b");
myThread myThread2=new myThread("b","a");
new Thread(myThread1,"t1").start();
new Thread(myThread2,"t2").start();
}
}
class myThread implements Runnable{
private String thread1;
private String thread2;
public myThread(String thread1, String thread2) {
this.thread1 = thread1;
this.thread2 = thread2;
}
@Override
public void run() {
synchronized (thread1){
System.out.println(Thread.currentThread().getName()+"lock:"+thread1+"----"+"get:"+thread2);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (thread2){
System.out.println(Thread.currentThread().getName()+"lock:"+thread2+"----"+"get:"+thread1);
}
}
}
}
上面是一个死锁的程序
线程1和2的a获得b之后,a被锁了,b在想获得a就形成死锁了。
检查死锁
1、jps -l 看活着的进程
2、jstack 进程号 查看堆栈
找到Found one java-level deadlock 然后就能查看死锁信息
十六、乐观锁&悲观锁
1、并发控制
当程序中可能出现并发的情况时,就需要通过一定的手段来保证在并发情况下数据的准确性,通过这种手段保证了当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的。这种手段就叫做并发控制。并发控制的目的是保证一个用户的工作不会对另一个用户的工作产生不合理的影响。
没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题。
无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。其实不仅仅是关系型数据库系统中有乐观锁和悲观锁的概念,像hibernate、tair、memcache等都有类似的概念。所以,不应该拿乐观锁、悲观锁和其他的数据库锁等进行对比。
2、悲观锁
当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”】。
百度百科:
悲观锁,正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
之所以叫做悲观锁,是因为这是一种对数据的修改抱有悲观态度的并发控制方式。我们一般认为数据被并发修改的概率比较大,所以需要在修改之前先加锁。
悲观锁主要分为共享锁或排他锁
- 共享锁【Shared lock】又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
- 排他锁【Exclusive lock】又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。
但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。
3、乐观锁
乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。
百度百科:
乐观锁机制采取了更加宽松的加锁机制。乐观锁是相对悲观锁而言,也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。
4、实现方式
1、悲观锁的实现方式
悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:
- 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
- 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
- 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
- 期间如果有其他对该记录做修改或加排他锁的操作,都会等待解锁或直接抛出异常。
拿比较常用的MySql Innodb引擎举例,来说明一下在SQL中如何使用悲观锁。
要使用悲观锁,必须关闭MySQL数据库的自动提交属性。因为MySQL默认使用autocommit模式,也就是说,当执行一个更新操作后,MySQL会立刻将结果进行提交。(sql语句:set autocommit=0)
以淘宝下单过程中扣减库存的需求说明一下悲观锁的使用:
以上,在对id = 1的记录修改前,先通过for update的方式进行加锁,然后再进行修改。这就是比较典型的悲观锁策略。
如果以上修改库存的代码发生并发,同一时间只有一个线程可以开启事务并获得id=1的锁,其它的事务必须等本次事务提交之后才能执行。这样可以保证当前的数据不会被其它事务修改。
上面提到,使用select…for update会把数据给锁住,不过需要注意一些锁的级别,MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。
2、乐观锁实现方式
使用乐观锁就不需要借助数据库的锁机制了。
乐观锁的概念中其实已经阐述了它的具体实现细节。主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是CAS(Compare and Swap)。
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
比如前面的扣减库存问题,通过乐观锁可以实现如下:
以上,在更新之前,先查询一下库存表中当前库存数(quantity),然后在做update的时候,以库存数作为一个修改条件。当提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。
(1)CAS
CAS:比较并交换,如果我期望的值达到了,那么就更新,否则就不更新。CAS是cpu的并发原语。
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticLockingDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger=new AtomicInteger(0);
for (int i = 0; i < 5; i++) {
atomicInteger.getAndIncrement();
System.out.println("atomicInteger = " + atomicInteger.get());
atomicInteger.compareAndSet(3,4);
if (atomicInteger.get()==4){
System.out.println("atomicInteger 3--->4 finshed integer=" + atomicInteger.get());
atomicInteger.compareAndSet(3,6);
System.out.println("atomicInteger 3--->6 finshed integer=" + atomicInteger.get());
break;
}
}
}
}
从结果可以看出来,只有当int=3的时候 才能更新为4,否则不会更新。里面的方法**compareAndSet()**就是CAS。
下面看他的底层代码
**compareAndSet()**的底层代码
**getAndIncrement()**底层代码
CAS: 比较当前工作内存中的值,和主内存中的值,如果这个值是期望的,那么则执行操作,如果不是就一直循环。(自旋锁)
缺点:
1、循环会耗时
2、一次性只能保证一个共享变量的原子性
3、ABA问题
(2)ABA
(3)原子引用
采用原子引用解决ABA问题
public class OptimisticLockingDemo {
private static AtomicInteger atomicInteger;
public static void main(String[] args) {
AtomicStampedReference<Integer> stampedReference=new AtomicStampedReference<>(2020,1);
new Thread(()->{
int stamp=stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"a1-->"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(stampedReference.compareAndSet(2020, 2021, stampedReference.getStamp(), stampedReference.getStamp() + 1));
int stamp2=stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"a2-->"+stamp2);
System.out.println(stampedReference.compareAndSet(2021, 2020, stampedReference.getStamp(), stampedReference.getStamp() + 1));
int stamp3=stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"a3-->"+stamp3);
}).start();
new Thread(()->{
int stamp=stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"b1-->"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
stampedReference.compareAndSet(2020,2021,stampedReference.getStamp(),stampedReference.getStamp()+1);
}).start();
}
}
反复试了好几次,都不行出错,后来找到原因是Integer的锅
解决方式就是把Integer设置的小点
public class OptimisticLockingDemo {
private static AtomicInteger atomicInteger;
public static void main(String[] args) {
AtomicStampedReference<Integer> stampedReference=new AtomicStampedReference<>(1,1);
new Thread(()->{
int stamp=stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"a1-->"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(stampedReference.compareAndSet(1, 2, stampedReference.getStamp(), stampedReference.getStamp() + 1));
int stamp2=stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"a2-->"+stamp2);
System.out.println(stampedReference.compareAndSet(2, 1, stampedReference.getStamp(), stampedReference.getStamp() + 1));
int stamp3=stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"a3-->"+stamp3);
}).start();
new Thread(()->{
int stamp=stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"b1-->"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(stampedReference.compareAndSet(1, 2, stampedReference.getStamp(), stampedReference.getStamp() + 1));
}).start();
}
}
stampedReference.compareAndSet(a,b,c,d)
(1)第一个参数expectedReference:表示预期值。
(2)第二个参数newReference:表示要更新的值。
(3)第三个参数expectedStamp:表示预期的时间戳。
(4)第四个参数newStamp:表示要更新的时间戳。
这里问题其实不大,一般开发过程中,<>里面都是存实体类,不会存Integer。。。
.getName()+“b1–>”+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
stampedReference.compareAndSet(2020,2021,stampedReference.getStamp(),stampedReference.getStamp()+1);
}).start();
}
}
[外链图片转存中...(img-jNRUExxE-1608616410052)]
反复试了好几次,都不行出错,后来找到原因是Integer的锅
[外链图片转存中...(img-MIwf9LxL-1608616410053)]
解决方式就是把Integer设置的小点
```java
public class OptimisticLockingDemo {
private static AtomicInteger atomicInteger;
public static void main(String[] args) {
AtomicStampedReference<Integer> stampedReference=new AtomicStampedReference<>(1,1);
new Thread(()->{
int stamp=stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"a1-->"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(stampedReference.compareAndSet(1, 2, stampedReference.getStamp(), stampedReference.getStamp() + 1));
int stamp2=stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"a2-->"+stamp2);
System.out.println(stampedReference.compareAndSet(2, 1, stampedReference.getStamp(), stampedReference.getStamp() + 1));
int stamp3=stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"a3-->"+stamp3);
}).start();
new Thread(()->{
int stamp=stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"b1-->"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(stampedReference.compareAndSet(1, 2, stampedReference.getStamp(), stampedReference.getStamp() + 1));
}).start();
}
}
[外链图片转存中…(img-FNV784Om-1608616410055)]
stampedReference.compareAndSet(a,b,c,d)
(1)第一个参数expectedReference:表示预期值。
(2)第二个参数newReference:表示要更新的值。
(3)第三个参数expectedStamp:表示预期的时间戳。
(4)第四个参数newStamp:表示要更新的时间戳。
这里问题其实不大,一般开发过程中,<>里面都是存实体类,不会存Integer。。。
成了!!!!