Java中如何进行加锁??

简介: Java中如何进行加锁??

笔者在上篇文章介绍了线程安全的问题,接下来本篇文章就是来讲解如何避免线程安全问题~~

前言:创建两个线程,每个线程都实现对同一个变量count各自自增5W次,我们来看一下代码:

class Counter{
    private int count=0;
    public void add(){
        count++;
    }
    public int get(){
        return count;
    }
}
public class Main2 {
    public static void main(String[] args) throws InterruptedException{
        Counter counter=new Counter();
        //搞两个线程,两个线程分别对这个count自增5W次
        //线程1
        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        //线程2
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        //启动线程
        t1.start();
        t2.start();
        //等待两个线程执行结束,然后看一下结果
        t1.join();
        t2.join();
        System.out.println(counter.get());
        //预期结果是10W,但是,实际结果像是一个随机值,每次执行的结果都不一样
    }
}

上述代码的运行结果是不确定的,是一个随即值,多次刷新重新运行,结果大概率是不一样的~,预期效果个代码的运行结果不一样,这就是Bug——》线程安全问题!

通过加锁来有效避免线程安全问题:

Synchronized是Java中的关键字,可以使用这个关键字来实现加锁的效果~

public void add(){
       // count++;
        synchronized (this){
            //这里的this可以写任意一个Object对象(基本数据类型不可)
            //此处写了this就相当于Counter counter=new Counter();中的counter
            count++;
        }
    }

那么,我们来看一下此时代码的运行结果~

符合我们预期的一个效果~

锁有两个核心的操作,加锁和解锁;

此处使用代码块的方式来表示:进入synchronized修饰的代码块的时候,就会触发加锁,出了synchronized代码块就会触发解锁,{ }就相当于WC~~

在上述代码中:synchronized(this)——》this是指:锁对象(在针对哪个对象?)!

如果两个线程针对同一个对象加锁,此时就会出现“锁竞争”(一个线程先拿到锁,另一个线程阻塞等待)!

如果两个线程针对不同的对象加锁,此时不好存在锁竞争,各种获取各自锁即可!

加锁本质上是把并发的变成了串行的~

join()和加锁不一样:

join()是让两个线程完整的进行串行~

加锁是让两个线程的某小部分串行了,大部分都是并发的!!

加锁:在保证线程安全的前提下,同时还能够让代码跑的更快一些,更好的利用CPU,无论如何,加锁都可能导致阻塞,代码阻塞对应程序的效率肯定还是会有影响的,此处虽然加锁了,比不加锁要慢点,肯定还是比串行要更快,同时比不加锁算得更准!!

如果直接给方法使用synchronized修饰,此时就相当于this为加锁对象!!

如果synchronized修饰静态方法static(),此时就算不给this加锁了,而是给类对象加锁!!

更常见的还是自己手动指定一个锁对象:

//自己手动指定锁对象
    private Object locker=new Object();
    public void add(){
        synchronized (locker){
            //这里的locker可以写任意一个Object对象(基本数据类型不可)
            count++;
        }
    }

要牢记:如果多个线程尝试对同一对象加锁,此时就会产生锁竞争!!针对不同的锁对象加锁,就不会有锁竞争~

另一个线程不安全的场景:由于内存可见性,所引起的线程不安全~

先写一个带有Bug的代码:

import java.util.Scanner;
public class Main3 {
    public static int flag=0;
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            while (flag==0){
                //空着,啥都没有
            }
            System.out.println("循环结束,t1结束");
        });
        Thread t2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("请输入一个整数: ");
            flag=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

对该段代码的预期效果:t1通过flag=0作为条件,进行循环,初始情况下,将进入循环,t2通过控制台输入一个整数,一旦用户输入非0的值,此时t1的循环就会立即结束,从而t1线程退出!!

但是,实际的效果:输入非0的值之后,t1线程并没有退出,循环没有结束!通过jconsole可以看到t1线程仍然在执行,处在RUNNABLE状态。

实际效果 !=预期效果——》这就是Bug

为啥有这个问题??这就是内存可见性的锅!!

所谓的内存可见性,就是多线程环境下,,编辑器对于代码优化产生了误判,从而引起了Bug,进一步导致了咱们的Bug,咱们的处理方式:就是让编辑器针对这个场景暂停优化!!使用Volatile关键字,被volatile修饰的变量,此时编辑器就会紧张上述优化,从而能够确保每次都是从内存中重新读取数据~

即:针对上述代码的更改:

volatile public static int flag=0;

加上volatile关键字之后,此时编辑器就能够保证每次都是重新从内存读取flag变量的值,此时t2修饰flag,t1就可以立即感知到了,因此t1就可以正确退出了~

volatile不保证原子性(注意)

volatile适用的场景是一个线程读,一个线程写的情况

synchronized则是多个线程写

volatile的这个效果称为:“保证内存可见性”

synchronized不确定能不能保证内存可见性

volatile还有一个效果:禁止指令重排序!指令重排序也是编辑器优化的策略(调整了代码执行的顺序,,让程序更高效,前台也是保证整体逻辑不变)

关于volatile和内存可见性的补充~

网上有效资料:线程修改一个变量,会把这个变量先从主内存读取到工作内存,然后修改工作内存的值,再写回到主内存中~

内存可见性:t1频繁读取主内存,效率比较低,就被优化成直接读取自己的工作内存,t1修改了主内存的结果,由于t1没有读取主内存导致修改不能被识别到!!

工作内存《——》CPU寄存器

主内存《——》内存

相关文章
|
5月前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
59 2
|
3月前
|
缓存 Java
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
本文介绍了几种常见的锁机制,包括公平锁与非公平锁、可重入锁与不可重入锁、自旋锁以及读写锁和互斥锁。公平锁按申请顺序分配锁,而非公平锁允许插队。可重入锁允许线程多次获取同一锁,避免死锁。自旋锁通过循环尝试获取锁,减少上下文切换开销。读写锁区分读锁和写锁,提高并发性能。文章还提供了相关代码示例,帮助理解这些锁的实现和使用场景。
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
|
3月前
|
Java 开发者
Java 中的锁是什么意思,有哪些分类?
在Java多线程编程中,锁用于控制多个线程对共享资源的访问,确保数据一致性和正确性。本文探讨锁的概念、作用及分类,包括乐观锁与悲观锁、自旋锁与适应性自旋锁、公平锁与非公平锁、可重入锁和读写锁,同时提供使用锁时的注意事项,帮助开发者提高程序性能和稳定性。
157 3
|
4月前
|
Java
Java 中锁的主要类型
【10月更文挑战第10天】
|
5月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
|
5月前
|
算法 Java 关系型数据库
Java中到底有哪些锁
【9月更文挑战第24天】在Java中,锁主要分为乐观锁与悲观锁、自旋锁与自适应自旋锁、公平锁与非公平锁、可重入锁以及独享锁与共享锁。乐观锁适用于读多写少场景,通过版本号或CAS算法实现;悲观锁适用于写多读少场景,通过加锁保证数据一致性。自旋锁与自适应自旋锁通过循环等待减少线程挂起和恢复的开销,适用于锁持有时间短的场景。公平锁按请求顺序获取锁,适合等待敏感场景;非公平锁性能更高,适合频繁加解锁场景。可重入锁支持同一线程多次获取,避免死锁;独享锁与共享锁分别用于独占和并发读场景。
|
6月前
|
小程序 Java 开发工具
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了
本文通过一个生动的例子,探讨了Java中加锁仍可能出现超卖问题的原因及解决方案。作者“JavaDog程序狗”通过模拟空调租赁场景,详细解析了超卖现象及其背后的多线程并发问题。文章介绍了四种解决超卖的方法:乐观锁、悲观锁、分布式锁以及代码级锁,并重点讨论了ReentrantLock的使用。此外,还分析了事务套锁失效的原因及解决办法,强调了事务边界的重要性。
177 2
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了
|
6月前
|
存储 Java
Java锁是什么?简单了解
在高并发环境下,锁是Java中至关重要的概念。锁或互斥是一种同步机制,用于限制多线程环境下的资源访问,确保排他性和并发控制。例如,超市储物柜仅能存放一个物品,若三人同时使用,则需通过锁机制确保每次只有一个线程访问。Java中可以通过`synchronized`关键字实现加锁,确保关键代码段的原子性,避免数据不一致问题。正确使用锁可有效提升程序的稳定性和安全性。
100 1
Java锁是什么?简单了解
|
5月前
|
Java 数据库
JAVA并发编程-一文看懂全部锁机制
曾几何时,面试官问:java都有哪些锁?小白,一脸无辜:用过的有synchronized,其他不清楚。面试官:回去等通知! 今天我们庖丁解牛说说,各种锁有什么区别、什么场景可以用,通俗直白的分析,让小白再也不怕面试官八股文拷打。
|
4月前
|
安全 Java 开发者
java的synchronized有几种加锁方式
Java的 `synchronized`通过上述三种加锁方式,为开发者提供了从粗粒度到细粒度的并发控制能力,满足了不同场景下的线程安全需求。合理选择加锁方式对于提升程序的并发性能和正确性至关重要,开发者应根据实际应用场景的特性和性能要求来决定使用哪种加锁策略。
60 0

热门文章

最新文章