一个并发问题:
一百张票,很多人抢,可能会出现超卖问题
public static int cnt = 100;
public static void main(String[] args) throws InterruptedException {
List<Thread> list = new ArrayList<>();
for (int i=0;i<1000;i++){
Thread thread = new Thread(){
@Override
public void run() {
if(cnt>0){
String name = Thread.currentThread().getName();
System.out.println(name+"抢到了"+cnt+"张票");
cnt--;
}
}
};
list.add(thread);
}
//启动
for (int i=0;i<list.size();i++){
list.get(i).start();
}
for (int i=0;i<list.size();i++){
list.get(i).join();
}
System.out.println("剩下的票:"+cnt);
}
第一个问题:cnt>0作为条件,会出现并发问题;
第二个问题:cnt在--的时候,也可能导致并发问题
使用锁进行解决
public static int cnt = 100;
//创建一把锁
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
List<Thread> list = new ArrayList<>();
for (int i=0;i<1000;i++){
Thread thread = new Thread(){
@Override
public void run() {
//使用锁将可能出现并发的代码锁住
synchronized (lock){
if(cnt>0){
String name = Thread.currentThread().getName();
System.out.println(name+"抢到了"+cnt+"张票");
cnt--;
}
}
}
};
list.add(thread);
}
//启动
for (int i=0;i<list.size();i++){
list.get(i).start();
}
for (int i=0;i<list.size();i++){
list.get(i).join();
}
System.out.println("剩下的票:"+cnt);
}
synchronized
基本使用:锁对象
所有的锁必须要使用同一把锁对象才能有结果
以下代码是无法锁住线程方法的,如果多线程,下面代码,每个线程对象都会使用自己的独立锁
public class TicketThread extends Thread{
public static int cnt = 100;
Object lock = new Lock();
@Override
public void run() {
//使用锁将可能出现并发的代码锁住
synchronized (lock){
if(cnt>0){
String name = Thread.currentThread().getName();
System.out.println(name+"抢到了"+cnt+"张票");
cnt--;
}
}
}
}
可以将Object lock使用static修饰,或者从外部传入同一把锁
修饰this对象
注意必须是多个线程使用一个对象的时候才有效果,一般用于单例模式
创建一个Runnable的任务
public class TickTask implements Runnable {
public int cnt = 100;
@Override
public void run() {
//使用锁将可能出现并发的代码锁住
synchronized (this){
if(cnt>0){
String name = Thread.currentThread().getName();
System.out.println(name+"抢到了"+cnt+"张票");
cnt--;
}
}
}
}
通过线程池,让多个线程调用同一个task
public static void main(String[] args) {
//线程池+runnable组合
ExecutorService service = Executors.newFixedThreadPool(10);
TickTask task = new TickTask();
for(int i=0;i<1000;i++){
service.execute(task);
}
}
修饰方法
一般情况需要使用static配合使用才有效果,因为锁住是类的方法,而不是对象的方法
如果是对象的方法,必须保证是同一个对象
一般情况最好是不要锁方法
修饰类的类对象:
每个类被JVM加载之后,都会产生一个类的对象,而且是唯一的
大多数情况直接锁对象都是有效果的,因为类对象是唯一的
synchronized(TicketThread.class){
if(cnt>0){
String name = Thread.currentThread().getName();
System.out.println(name+"抢到了"+cnt+"张票");
cnt--;
}
}
synchronized基本原理
是利用synchronized去锁住一个对象,必须要判断这个对象是不是同一个对象,通过monitor对锁进行监控,实现争抢锁的实现逻辑
锁的信息是利用对象Head信息比较,其中MarkWord用于储存对象自身的运行是数据
每次有线程需要获取锁的时候,都需要将锁的信息提供给monitor进行查询
如果锁对象运行时数据发生变化的时候,可能会导致锁对象在比较时发生不一致的情况
结论,不要修改锁储存的数据,以避免锁失效
Lock手动锁的作用
synchronized是自动锁,什么时候加锁,什么时候释放锁都是系统完成,如果使用不当,会出现死锁
创建一个男孩线程去找女孩约会
public class Boy extends Thread {
@Override
public void run() {
try {
date();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void date() throws InterruptedException {
//男孩只能去找一个女孩约会
synchronized (Boy.class){
System.out.println("男去找女孩约会了");
Thread.sleep(10);
//加上锁,防止女孩同和两个男孩约会
synchronized(Girl.class){
System.out.println("女孩在约会中...");
}
System.out.println("在一起约会了...");
}
}
}
创建一个女孩去找男孩约会
public class Girl extends Thread{
@Override
public void run() {
try {
date();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void date() throws InterruptedException {
//女孩只能去找一个男孩约会
synchronized (Girl.class){
System.out.println("女孩去找男孩约会了");
//防止男孩和多个女孩约会
synchronized (Boy.class){
System.out.println("男孩在约会了");
}
System.out.println("在一起约会了");
}
}
}
开始约会
public static void main(String[] args) throws InterruptedException {
new Boy().start();
new Girl().start();
}
Lock是JDK提供的锁对象,可以手动控制锁的加载和释放
lock是一个接口
public interface Lock {
void lock(); //主动锁住代码,会阻塞代码
void lockInterruptibly() throws InterruptedException;//可以中断的锁
boolean tryLock();//尝试获取锁,非阻塞锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //尝试获取锁,指定一个等待时间
void unlock();//释放锁
Condition newCondition();//条件锁,可以分为读锁和写锁
}
ReentrantLock的基本使用
public class TestLock {
static int cnt =10;
public static void main(String[] args) {
Lock lock = new ReentrantLock();
//创建一个任务
Runnable runnable = ()->{
try{
lock.lock();//当前线程主动获取锁,如果其他线程已经获取了锁,那么会阻塞
if(cnt>0){
System.out.println("抢到资源"+cnt);
cnt--;
if(cnt==5){
throw new RuntimeException();
}
}
}finally { //无论是否抛出异常,都释放锁
lock.unlock();// 释放锁
}
};
//创建线程池执行任务
ExecutorService service = Executors.newFixedThreadPool(5);
for (int i=0;i<100;i++){
service.execute(runnable);
}
service.shutdown();
}
}
tryLock的使用
尝试获取锁,拿到了返回true,失败返回false
Lock lock = new ReentrantLock();
//创建一个任务
Runnable runnable = ()->{
if(lock.tryLock()){//尝试获取锁,不会阻塞线程
try{
if(cnt>0){
System.out.println("抢到资源"+cnt);
cnt--;
}
} finally { //无论是否抛出异常,都释放锁
lock.unlock();// 释放锁
}
}else{
System.out.println("没有抢到资源");
}
};
可带时间参数,在获取锁的时候,可以设置等待时间
//创建一个任务
Runnable runnable = () -> {
try {
//第一个参数是等待多长时间
//第二个参数是等待时间的单位
if (lock.tryLock(1,TimeUnit.MICROSECONDS)) {
try {
if (cnt > 0) {
System.out.println("抢到资源" + cnt);
cnt--;
}
}finally {
lock.unlock();
}
} else {
System.out.println("没有抢到资源");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
-
lockInterruptibly:可以中断锁
public static void main(String[] args) throws InterruptedException { Lock lock = new ReentrantLock(); Runnable task = new Runnable() { @Override public void run() { try { lock.lockInterruptibly();//可以中断 try { String name = Thread.currentThread().getName(); System.out.println(name + "执行任务"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } catch (InterruptedException e) { // e.printStackTrace(); System.out.println("不想再等待了"); } } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); Thread.sleep(1000); t2.interrupt(); //中断线程2的等待 }
锁的分类
可重载锁:一个已经获得锁的线程,可以重复在加载锁class MyClass { public synchronized void method1() { method2(); } public synchronized void method2() { } }
method1和method2都所有锁,一个线程加载method1的锁之后,还可以继续加载method2的锁,如果不是可重载锁的话就不能
可中断锁:A线程获取了锁,B线程在等待锁的时候,可以直接中断等待 - 公平锁:使得多个线程在获取锁的机会上是公平的
-
Lock lock = new ReentrantLock(true);//true是公平,默认false是不公平
读写锁:
- 可以让数据读取和修改分开的锁
- 当一个读取数据的线程获取锁的时候,另一个数据如果是读取数据,不需要锁
- 当一个读取数据的线程获取锁的时候,另一个线程如果是修改数据,不需要锁
- 当一个写入数据的线程获取锁的时候,另一个线程无论是读取还是修改都需要锁
- 基本规则
- 写-写:互斥,阻塞
- 读-写:互斥,读阻塞写,写阻塞读
- 读读:不互斥,不阻塞
- 读写分类
-
public class MyData2 { int value = 10; //创建读写锁 ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); //分为写锁 ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); //读锁 ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); public int getValue() { readLock.lock(); try{ Thread.sleep(1000); return value; } catch (InterruptedException e) { e.printStackTrace(); } finally { readLock.unlock(); } return 0; } public void setValue(int value) { writeLock.lock(); try{ Thread.sleep(1000); this.value = value; } catch (InterruptedException e) { e.printStackTrace(); } finally { writeLock.unlock(); } } }
线程通信:
- 在使用多线程开发的时候,会发现线程运行的过程是无序的,无法对线程进行精准的控制,但是有些场景是需要对线程进行控制的,例如生产者-消费-商品的问题
- 创建商品
-
public class Product { int id; String brand;//标签,例如北京,长沙 String name;//名称,例如烤鸭,臭豆腐 }
创建生产者线程
-
//生产者线程 public class ProductThread extends Thread{ Product product;//操作的商品 public ProductThread(Product product) { this.product = product; } @Override public void run() { for (int i=1;i<=10;i++){ product.setId(i); if(i%2==0){ product.setBrand("长沙"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } product.setName("臭豆腐"); }else{ product.setBrand("北京"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } product.setName("烤鸭"); } System.out.println("厨师加工了"+product.getId()+"-"+product.getBrand()+product.getName()); } } }
创建消费者线程
-
public class ConsumerThread extends Thread{ Product product;//操作的商品 public ConsumerThread(Product product) { this.product = product; } @Override public void run() { for (int i=1;i<=10;i++){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("顾客吃了:"+product.getId()+"-"+product.getBrand()+product.getName()); } } }
通过synchronized+notify/wait来控制线程的顺序
-
给生产者添加
-
public class ProductThread extends Thread{ Product product;//操作的商品 public ProductThread(Product product) { this.product = product; } @Override public void run() { for (int i=1;i<=10;i++){ synchronized (product){ product.setId(i); if(i%2==0){ product.setBrand("长沙"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } product.setName("臭豆腐"); }else{ product.setBrand("北京"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } product.setName("烤鸭"); } System.out.println("厨师加工了"+product.getId()+"-"+product.getBrand()+product.getName()); product.notify(); try { //放弃锁,进入到等待的状态 product.wait(); //阻塞状态 } catch (InterruptedException e) { e.printStackTrace(); } } } } }
给消费者添加
Product product;//操作的商品 public ConsumerThread(Product product) { this.product = product; } @Override public void run() { for (int i=1;i<=10;i++){ synchronized (product){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("顾客吃了:"+product.getId()+"-"+product.getBrand()+product.getName()); product.notify();//告诉厨师可以继续开始做了 try { product.wait();//客户进入等待状态 } catch (InterruptedException e) { e.printStackTrace(); } } } } }
-
- 可以让数据读取和修改分开的锁