在上一篇博客中的为解决线程安全问题引出了加锁操作,今天我总结一下有关加锁基础的知识点,看看是否能帮你解决问题
目录
互斥
在synchronized中的“同步”指的就是“互斥”,synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待
我首先来解决上一篇博客代码的BUG很简单就是将方法加锁
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
在加锁的情况下,线程的执行三个指令岔开,保证了结果的准确性
synchronized使用方法
1.修饰普通方法
public class counter {
public synchronized void func() {
}
}
2.修饰静态方法
public class counter {
public synchronized static void func() {
}
}
3.修饰代码块
1)锁当前对象
锁当前对象与普通方法加锁同理
public class counter {
public void func() {
synchronized(this){//this表示对对象加锁
}
}
}
当多个线程调用func()方法其实是对counter对象加锁,此时一个线程获取到锁了其他线程就要阻塞等待。
当多个线程对于不同的对象加锁时则不会出现互斥现象
2)锁类对象
锁类对象与静态方法加锁同理
public class counter {
public void func() {
synchronized(counter.class){//对counter类加锁
}
}
}
对类对象加锁整个JVM里只有一个
注意
在Java中任何对象都可以是锁对象,每个对象在内存中有特殊区域,对象头(JVM自带的用于保存对象的特殊信息)
只有两个线程竞争同一把锁的时候才会产生阻塞等待
volatile关键字
volatile能够保证内存可见性
看一下这段代码
public class Demo14 {
static class Counter{
public int flag=0
}
public static void main(String[] args) {
Counter counter=new Counter();
Thread t1=new Thread(()->{
while(counter.flag==0) {
}
System.out.println("t1 end");
});
t1.start();
Thread t2=new Thread(()->{
Scanner sca=new Scanner(System.in);
System.out.println("请输入数字");
counter.flag=sca.nextInt();
});
t2.start();
}
}
我们希望通过用户输入非零打印t1 end,我们来看结果
可以看到输入非零后并没有结束,原因就是编译器在不断的读取中产生了优化,此时就要使用关键字volatile来禁止编译器优化,会给对应的变量加上特定的指令(内存屏障)
volatile于synchronized区别
volatile解决的是一个线程读一个线程写的问题,禁止编译器优化保证内存可见性/禁止指令重排序
synchronized解决的是两个线程写的问题,加锁保证原子性
wait和notify
wait和notify用来控制线程的执行顺序
wait:等待,调用wait的线程就会进入阻塞等待状态(WAITING)
notify:通知/唤醒,调用notify就可以把对应的wait线程给唤醒(从阻塞恢复到就绪状态)
wait()和notify()都是Object的成员方法
wait和notify内部的执行过程
1.释放锁:前提是需要先获取到锁,wait需要放到synchronized中使用并且锁对象和调用wait()方法的对象要是同一个对象
2.等待通知
3.通知到达之后就会唤醒,并且尝试重新获取锁
notify就只有进行通知
//体现wait和notify的运行顺序
public class Demo {
public static Object locker=new Object();
public static void main(String[] args) {
Thread t1=new Thread(()->{
synchronized (locker) {
try {
System.out.println("wait 开始");
locker.wait();
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入内容");
scanner.next();
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
});
t2.start();
}
}
可以看一下运行结果
这里要注意执行过程和必须是相同的对象
阻塞状态
WAITING状态下务必其他线程来主动唤醒
BLOCKED状态下其他线程释放锁后操作系统来唤醒
TIMED_WAITING状态下操作系统计时并唤醒
notifyAll()方法
wait()和sleep()的对比
相同是都会让线程等待,一个是用于线程之间的通信的,一个是让线程阻塞一段时间,
wait()需要搭配synchronized使用sleep()不需要
wait()是Object 的方法sleep()是 Thread 的静态方法
多线程案例
单例模式
单例模式是一种常见的设计模式,目的是为了某个类在程序中只有一个实例
饿汉模式(加载类的时候就会创建实例)
class Singleton{
/*
* 饿汉模式
* */
private static Singleton instance=new Singleton();//静态成员变量实现类的实例
public static Singleton getInstance() {//通过getInstance创建对象
return instance;
}
private Singleton(){}//私有的构造方法
}
通过使用静态成员表示实例(唯一性)+让构造方法私有(防止new对象),当Singleton类加载就会实例化对象就称为饿汉模式
懒汉模式(首次使用实例时才会创建实例)
class SingletonLazy{
/*
* 懒汉模式 线程不安全只是在初始实例化时会产生 解决加锁
* */
volatile private static SingletonLazy instance=null;
//禁止优化
public static SingletonLazy getInstance() {
if(instance == null )//是否初始化好了减少锁竞争
synchronized(Demo.class) {//保证读和写操作是原子的
if(instance==null) {//判断是否真要初始化
instance=new SingletonLazy();
}
}
return instance;
}
private SingletonLazy(){}
}
为解决线程安全问题
1.加锁 synchronized
2.双重if判定(外层if降低加锁冲突,里层if真正判定是否要实例化)
3.volatile(防止优化)
阻塞队列
阻塞队列是保证线程安全的数据结构,如果队列为空出队会阻塞,如果队列为满入队会阻塞
无锁队列:更高效但消耗更多CPU资源,内部没有锁也能保证线程安全
消息队列:在队列中包含多种“类型”元素,取元素时按照类型先进先出
阻塞队列最重要的应用场景:生产者消费者模型
生产者消费者模型
AB不会直接交互,一方有问题不会影响到另一方,更好的解耦合,同时阻塞队列相当于一个缓冲区,平衡生产者和消费者的处理能力(削峰填谷)
Java标准库的阻塞队列(BlockingQueue)
注意
1.BlockingQueue 是一个接口. 真正实现的类是 Array/LinkedBlockingQueue
2.put()方法用于阻塞式的入队列, take()用于阻塞式的出队列
3.像offer(),poll(),peek()等方法也支持但是不具备阻塞特性
代码
public static void main(String[] args) {
BlockingQueue<Integer> queue=new LinkedBlockingQueue<>();
Thread t1=new Thread(()->{//消费者
while(true) {
try {
int tmp=queue.take();
System.out.println("消费者:"+tmp);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
Thread t2=new Thread(()->{//生产者
int n=0;
while(true) {
try {
System.out.println("生产者:"+n);
queue.put(n);
n++;
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
}
可以看到结果为
知道了标准库的BlockingQueue我们试着模拟实现一下
模拟实现
class MYBlockingQueue {
private int[] items=new int[1000];
private int head=0;//队首
private int tail=0;//队尾
volatile private int size=0;
public void put(int value) {//入队
synchronized(this) {
while(this.size== items.length) { //队列已满
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
items[this.tail]=value;
this.tail++;
if(this.tail==items.length) {//到达数组末尾就要从头计算
tail = 0;
}
//tail=tail% items.length;//也可以使用
size++;
this.notify();//唤醒等待(队列不为空唤醒)
}
}
public Integer take() {//出队
int tmp=0;
synchronized (this) {
while(size==0) {
try {
this.wait();//发现队列为空等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
tmp=items[head];
this.head++;
if(this.head== items.length) {
head=0;
}
size--;
this.notify();//唤醒等待(队列满了出元素后唤醒)
}
return tmp;
}
}
定时器 Timer
Java.util里的一个组件,Timer 类的核心方法为schedule(),这个方法两个参数,第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒)
代码
public static void main(String[] args) throws InterruptedException {
Timer timer=new Timer();//java util的组件
//schedule 这个方法效果是安排一个任务
timer.schedule(new TimerTask() {//重写run()方法
@Override
public void run() {
System.out.println("一个要执行的任务");
}
},3000);
}
模拟实现定时器
class MyTask implements Comparable<MyTask> {
private Runnable command;
private long time;
public long getTime() {
return time;
}
public MyTask(Runnable command, long after) {
this.command=command;
this.time=System.currentTimeMillis()+after;//当前时间戳+等待时间=绝对时间
}
public void run() {//执行任务的方法直接通过Runnable 的run方法
command.run();
}
@Override
public int compareTo(MyTask o) {//比较规则
return (int) (this.time-o.time);//时间小的在前
}
}
//自己实现的定时器类
class MyTimer{
private Object locker=new Object();//锁对象
//带优先级的阻塞队列 java标准库给的
private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
//command 要执行的任务
//after 多长时间后来执行任务
public void schedule(Runnable command ,long after) {
MyTask myTask=new MyTask(command, after);
synchronized (locker){//扩大范围提高原子性
queue.put(myTask);
locker.notify();
}
}
public MyTimer() {
//在构造方法中启动一个线程
Thread t=new Thread(()->{
while(true) {
try {
synchronized (locker) {//目的是为了在执行任务中不会有其他任务加入
while(queue.isEmpty()) {//防止开始队列为空造成死锁
locker.wait();
}
MyTask myTask = queue.take();//取出队头任务
long curTime = System.currentTimeMillis();
if (myTask.getTime() > curTime) {
//时间未到
queue.put(myTask);//返回到队列中
locker.wait(myTask.getTime() - curTime);
} else {
//时间到了
myTask.run();//执行任务
}
}
} catch(InterruptedException e){
e.printStackTrace();
}
}
});
t.start();
}
}
线程池
我们知道进程本身就可以做到并发编程哪为什么要有线程呢?原因就是进程创建/销毁的开销大,那么线程池就是为了解决线程的频繁创建/销毁,线程池最大的好处就是减少每次启动、销毁线程的损耗
Java标准库的线程池
ExecutorService threadPool= Executors.newFixedThreadPool(10);
使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池
借助静态方法来创建实例像这样的方法称为“工厂方法”对应的设计模式称为“工厂模式”
Executors里面的各种工厂方法是针对ThreadPoolExecutor这个类进行new实例并传参来实例对象,因为类的构造方法限制太多了不方便使用工厂方法就是对构造方法的封装
Executors 创建线程池的方式
代码
public static void main(String[] args) {
ExecutorService threadPool= Executors.newFixedThreadPool(10);
threadPool.submit(new Runnable() {//核心方法submit()
@Override
public void run() {//重写run()方法
}
});
}
模拟实现
class MyThreadPool {
private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();//通过阻塞队列来保存要完成的任务
//核心方法,往线程池里插入任务
public void submit(Runnable runnable) {
try {
queue.put(runnable);//生产者
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public MyThreadPool(int n){
//在构造方法中需要创建线程完成上述要执行任务的工作
for(int i=0;i<n;i++) {//设置线程池线程个数n
Thread t=new Thread(()->{
while(!Thread.currentThread().isInterrupted()) {
try {
Runnable runnable=queue.take();
runnable.run();//执行任务 消费者
} catch (InterruptedException e) {
e.printStackTrace();
break;//出现中断跳出循环
}
}
});
t.start();
}
}
}
好的以上就是加锁的一些基础知识,我会在下一篇博客来总结加锁的进阶知识,觉得有用的还请点赞评论 蟹蟹!!!