精华内容
下载资源
问答
  • 2016-09-28 21:50:01

    多个线程多个锁:多个线程,每个线程都可以拿到自己指定的锁,分别获得锁之后,执行synchronized方法体内容。

    关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当做锁,哪个线程先执行synchronized关键字的方法,那个线程就持有该方法所属对象的锁(Lock),两个对象,线程获得的就是两个不同的锁,他们互不影响。

    有一种特殊情况则是相同的锁,即在静态方法上加synchronized关键字,表示锁定class类,类一级别的锁(独占.class类)


    从打印的结果我们可以看出,输出的结果有问题。因为我们在printNum方法上加了synchronized锁,那么应该是线程m1输出tag b,set count over!、taga,count=100之后才会打印b的信息。为什么会造成这样的结果呢???

    原来我们声明了t1和t2两个对象,在m1线程中调取t1对象,拥有的是t1对象的锁,而m2线程中调取t2对象,拥有的是t2对象的锁。关键字synchronized取得的锁都是对象锁,这样他们就互不影响,t1和t2的信息都保存在自己的线程栈中,对其他线程不可见,所有m1和m2并发执行。

    如果在printNum方法上加上static关键字,,表示锁定class类。不管声明多少个MultiThread引用,printNum方法跟随类存放在堆上,线程间会共享资源,输出结果会等m1信息输出结束之后才会开始输出m2信息。


    总结:

    一个对象一把锁

    在静态方法上synchronized代表的是类的锁


    本人在码云上提交了关于阅读spring源码之后编写的一些基础模块的实现,https://git.oschina.net/73hulu/spring.git 望大家不吝给颗星star,谢谢大家

    更多相关内容
  • java 多个线程同时写同一个文件

    千次阅读 2019-10-11 17:00:17
    话不说,先直接上代码: 主方法: import java.util.concurrent.CountDownLatch; /** * @ProjectName: emp_customer * @Package: PACKAGE_NAME * @ClassName: Test * @Author: Administrator * @...

    话不多说,先直接上代码:

    主方法:

    import java.util.concurrent.CountDownLatch;
    
    /**
     * @ProjectName: emp_customer
     * @Package: PACKAGE_NAME
     * @ClassName: Test
     * @Author: Administrator
     * @Description: ${description}
     * @Date: 2019/10/11 14:10
     * @Version: 1.0
     */
    public class Test {
         public static void main(String args[]){
    
             //线程数
             int threadSize=4;
             //源文件地址
             String sourcePath = "E:\\1\\4.txt";
             //目标文件地址
             String destnationPath = "E:\\2\\4.txt";
             //
             CountDownLatch latch = new CountDownLatch(threadSize);
             MultiDownloadFileThread m = new MultiDownloadFileThread(threadSize, sourcePath, destnationPath, latch);
             long startTime = System.currentTimeMillis();
             try {
                 m.excute();
                 latch.await();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             long endTime = System.currentTimeMillis();
             System.out.println("全部下载结束,共耗时" + (endTime - startTime) / 1000 + "s");
         }
    
    }
    

     

    线程类:

    import java.io.*;
    import java.nio.channels.FileChannel;
    import java.nio.channels.FileLock;
    import java.util.concurrent.CountDownLatch;
    
    /**
     * @ProjectName: emp_customer
     * @Package: PACKAGE_NAME
     * @ClassName: MultiDownloadFileThread
     * @Author: Administrator
     * @Description: ${description}
     * @Date: 2019/10/11 15:03
     * @Version: 1.0
     */
    public class MultiDownloadFileThread {
    
        private int threadCount;
        private String sourcePath;
        private String targetPath;
        private CountDownLatch latch;
    
        public MultiDownloadFileThread(int threadCount, String sourcePath, String targetPath, CountDownLatch latch) {
            this.threadCount = threadCount;
            this.sourcePath = sourcePath;
            this.targetPath = targetPath;
            this.latch = latch;
        }
    
        public void excute() {
            File file = new File(sourcePath);
            int fileLength = (int) file.length();
            //分割文件
            int blockSize = fileLength / threadCount;
            for (int i = 1; i <= threadCount; i++) {
                //第一个线程下载的开始位置
                int startIndex = (i - 1) * blockSize;
                int endIndex = startIndex + blockSize - 1;
                if (i == threadCount) {
                    //最后一个线程下载的长度稍微长一点
                    endIndex = fileLength;
                }
                System.out.println("线程" + i + "下载:" + startIndex + "字节~" + endIndex + "字节");
                new DownLoadThread(i, startIndex, endIndex).start();
            }
        }
    
    
        public class DownLoadThread extends Thread {
            private int i;
            private int startIndex;
            private int endIndex;
    
            public DownLoadThread(int i, int startIndex, int endIndex) {
                this.i = i;
                this.startIndex = startIndex;
                this.endIndex = endIndex;
            }
    
            @Override
            public void run() {
                File file = new File(sourcePath);
                FileInputStream in = null;
                RandomAccessFile raFile = null;
                FileChannel fcin = null;
                FileLock flin = null;
                try {
                    in = new FileInputStream(file);
                    in.skip(startIndex);
                    //给要写的文件加锁
                    raFile = new RandomAccessFile(targetPath, "rwd");
                    fcin =raFile.getChannel();
                    while(true){
                        try {
                            flin = fcin.tryLock();
                            break;
                        } catch (Exception e) {
                            System.out.println("有其他线程正在操作该文件,当前线程休眠1000毫秒,当前进入的线程为:"+i);
                            sleep(1000);
                        }
                    }
                    //随机写文件的时候从哪个位置开始写
                    raFile.seek(startIndex);
                    int len = 0;
                    byte[] arr = new byte[1024];
                    //获取文件片段长度
                    int segLength = endIndex - startIndex + 1;
                    while ((len = in.read(arr)) != -1) {
                        if (segLength > len) {
                            segLength = segLength - len;
                            raFile.write(arr, 0, len);
                        } else {
                            raFile.write(arr, 0, segLength);
                            break;
                        }
                    }
                    System.out.println("线程" + i + "下载完毕");
                    //计数值减一
                    latch.countDown();
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException | InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        if (in != null) {
                            in.close();
                        }
                        if (raFile != null) {
                            raFile.close();
                        }
    
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    运行结果:

     

    涉及到的相关知识点:

    1.CountDownLatch 

    2.RandomAccessFile

    3.FileLock

    下面我们具体讲解下

    一、FileLock :文件锁

    FileLock是java 1.4 版本后出现的一个类,它可以通过对一个可写文件(w)加锁,保证同时只有一个进程可以拿到文件的锁,这个进程从而可以对文件做访问;而其它拿不到锁的进程要么选择被挂起等待,要么选择去做一些其它的事情, 这样的机制保证了众进程可以顺序访问该文件。

    1. 概念

    • 共享锁: 共享读操作,但只能一个写(读可以同时,但写不能)。共享锁防止其他正在运行的程序获得重复的独占锁,但是允许他们获得重复的共享锁。
    • 独占锁: 只有一个读或一个写(读和写都不能同时)。独占锁防止其他程序获得任何类型的锁。

    2. lock()和tryLock()的区别:

    lock()阻塞的方法,锁定范围可以随着文件的增大而增加。无参lock()默认为独占锁;有参lock(0L, Long.MAX_VALUE, true)为共享锁。
    tryLock()非阻塞,当未获得锁时,返回null.
    3. FileLock的生命周期:在调用FileLock.release(),或者Channel.close(),或者JVM关闭

    4. FileLock是线程安全的
     

    二、RandomAccessFile

    java除了File类之外,还提供了专门处理文件的类,即RandomAccessFile(随机访问文件)类。该类是Java语言中功能最为丰富的文件访问类,它提供了众多的文件访问方法。RandomAccessFile类支持“随机访问”方式,这里“随机”是指可以跳转到文件的任意位置处读写数据。在访问一个文件的时候,不必把文件从头读到尾,而是希望像访问一个数据库一样“随心所欲”地访问一个文件的某个部分,这时使用RandomAccessFile类就是最佳选择。

    RandomAccessFile对象类有个位置指示器,指向当前读写处的位置,当前读写n个字节后,文件指示器将指向这n个字节后面的下一个字节处。刚打开文件时,文件指示器指向文件的开头处,可以移动文件指示器到新的位置,随后的读写操作将从新的位置开始。RandomAccessFile类在数据等长记录格式文件的随机(相对顺序而言)读取时有很大的优势,但该类仅限于操作文件,不能访问其他的I/O设备,如网络、内存映像等。RandomAccessFile类的构造方法如下所示:

    RandomAccessFile(File file ,  String mode)
    //创建随机存储文件流,文件属性由参数File对象指定

    RandomAccessFile(String name ,  String mode)
    //创建随机存储文件流,文件名由参数name指定

    这两个构造方法均涉及到一个String类型的参数mode,它决定随机存储文件流的操作模式,其中mode值及对应的含义如下:

    “r”:以只读的方式打开,调用该对象的任何write(写)方法都会导致IOException异常
    “rw”:以读、写方式打开,支持文件的读取或写入。若文件不存在,则创建之。
    “rws”:以读、写方式打开,与“rw”不同的是,还要对文件内容的每次更新都同步更新到潜在的存储设备中去。这里的“s”表示synchronous(同步)的意思
    “rwd”:以读、写方式打开,与“rw”不同的是,还要对文件内容的每次更新都同步更新到潜在的存储设备中去。使用“rwd”模式仅要求将文件的内容更新到存储设备中,而使用“rws”模式除了更新文件的内容,还要更新文件的元数据(metadata),因此至少要求1次低级别的I/O操作

     

    三、CountDownLatch

    1.概念

    • countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
    • 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

    2.源码

    • countDownLatch类中只提供了一个构造器:
    //参数count为计数值
    public CountDownLatch(int count) {  };  
    
    • 类中有三个方法是最重要的:
    //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
    public void await() throws InterruptedException { };   
    //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
    public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
    //将count值减1
    public void countDown() { };  

    假如在我们的代码里面,我们把main方法里面的

    latch.await();

    注释掉

    如下所示:

    我们可以看到跟之前的输出结果相比,我们的主方法里面输出的:全部下载结束的输出信息,已经打印到我们执行文件下载的线程输出信息的前面了,说明主线程先执行完。这从而说明,await() 方法具有阻塞作用

     我们在把latch.await();放开,把文件下载线程里的latch.countDown();注释掉,

    如下:

    我们可以看到,主程序里的的输出;全部下载结束的输出信息,一直未输出,程序也一直未结束,由此可得,countDown() 方法具有唤醒阻塞线程的作用。

    那么如何让 CountdownLatch 尽早结束

    假如我们的程序执行到countDown()之前就抛出异常,这就可能导致一整情况,CountdownLatch 计数永远不会达到零并且 await() 永远不会终止。

    为了解决这个问题,我们在调用 await() 时添加一个超时参数。

     

    CountDownLatch总结:

        1、CountDownLatch end = new CountDownLatch(N); //构造对象时候 需要传入参数N

      2、end.await()  能够阻塞线程 直到调用N次end.countDown() 方法才释放线程,最好设置超时参数

      3、end.countDown() 可以在多个线程中调用  计算调用次数是所有线程调用次数的总和

     

    对于,本demo而言,加不加文件锁的意义不大,因为在进入线程写的时候,就已经告诉单个线程需要写的内容是哪一块到哪一块,不加锁,也会正常写入,切经本人测试无误,但若是对同一个文件,即要写,又要读话,就必须加锁,不然程序执行可能不完整,具体情况可以查看下面的这个博客:https://blog.csdn.net/gxy3509394/article/details/7435993

    展开全文
  • 阿里面试失败后,气之下我图解了Java中18把锁

    万次阅读 多人点赞 2021-06-17 23:21:47
    目录 乐观和悲观 独占和共享 互斥和读写 公平和非公平 可重入 ...自旋 ...分段 ...升级(无锁|偏向|轻...回到代码世界中,一个共享数据加了悲观,那线程每次想操作这数据前都会假设其他线程.

     号外号外!《死磕 Java 并发编程》系列连载中,大家可以关注一波:

    「死磕 Java 并发编程04」说说Java Atomic 原子类的实现原理

    「死磕 Java 并发编程03」阿里二面,面试官:说说 Java CAS 原理?

    「死磕 Java 并发编程02」面试官:说说什么是 Java 内存模型(JMM)?

    「死磕 Java 并发编程01」10张图告诉你Java并发多线程那些破事

     

    目录

    乐观锁和悲观锁

    独占锁和共享锁

    互斥锁和读写锁

    公平锁和非公平锁

    可重入锁

    自旋锁

    分段锁

    锁升级(无锁|偏向锁|轻量级锁|重量级锁)

    锁优化技术(锁粗化、锁消除)

    一张图总结:


     

    乐观锁和悲观锁

    悲观锁

    悲观锁对应于生活中悲观的人,悲观的人总是想着事情往坏的方向发展。

    举个生活中的例子,假设厕所只有一个坑位了,悲观锁上厕所会第一时间把门反锁上,这样其他人上厕所只能在门外等候,这种状态就是「阻塞」了。

    回到代码世界中,一个共享数据加了悲观锁,那线程每次想操作这个数据前都会假设其他线程也可能会操作这个数据,所以每次操作前都会上锁,这样其他线程想操作这个数据拿不到锁只能阻塞了。

    20210606232504-2021-06-06-23-25-04

    在 Java 语言中 synchronizedReentrantLock等就是典型的悲观锁,还有一些使用了 synchronized 关键字的容器类如 HashTable 等也是悲观锁的应用。

    乐观锁

    乐观锁 对应于生活中乐观的人,乐观的人总是想着事情往好的方向发展。

    举个生活中的例子,假设厕所只有一个坑位了,乐观锁认为:这荒郊野外的,又没有什么人,不会有人抢我坑位的,每次关门上锁多浪费时间,还是不加锁好了。你看乐观锁就是天生乐观!

    回到代码世界中,乐观锁操作数据时不会上锁,在更新的时候会判断一下在此期间是否有其他线程去更新这个数据。

    20210606232434-2021-06-06-23-24-35

    乐观锁可以使用版本号机制CAS算法实现。在 Java 语言中 java.util.concurrent.atomic包下的原子类就是使用CAS 乐观锁实现的。

    两种锁的使用场景

    悲观锁和乐观锁没有孰优孰劣,有其各自适应的场景。

    乐观锁适用于写比较少(冲突比较小)的场景,因为不用上锁、释放锁,省去了锁的开销,从而提升了吞吐量。

    如果是写多读少的场景,即冲突比较严重,线程间竞争激励,使用乐观锁就是导致线程不断进行重试,这样可能还降低了性能,这种场景下使用悲观锁就比较合适。

    独占锁和共享锁

    独占锁

    独占锁是指锁一次只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。

    20210606232544-2021-06-06-23-25-45

    JDK中的synchronizedjava.util.concurrent(JUC)包中Lock的实现类就是独占锁。

    共享锁

    共享锁是指锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。

    20210606232612-2021-06-06-23-26-13

    在 JDK 中 ReentrantReadWriteLock 就是一种共享锁。

    互斥锁和读写锁

    互斥锁

    互斥锁是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。

    20210606232634-2021-06-06-23-26-35

    互斥锁一次只能一个线程拥有互斥锁,其他线程只有等待。

    读写锁

    读写锁是共享锁的一种具体实现。读写锁管理一组锁,一个是只读的锁,一个是写锁。

    读锁可以在没有写锁的时候被多个线程同时持有,而写锁是独占的。写锁的优先级要高于读锁,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。

    读写锁相比于互斥锁并发程度更高,每次只有一个写线程,但是同时可以有多个线程并发读。

    20210606232658-2021-06-06-23-26-59

    在 JDK 中定义了一个读写锁的接口:ReadWriteLock

    public interface ReadWriteLock {
        /**
         * 获取读锁
         */
        Lock readLock();
    
        /**
         * 获取写锁
         */
        Lock writeLock();
    }
    

    ReentrantReadWriteLock 实现了ReadWriteLock接口,具体实现这里不展开,后续会深入源码解析。

    公平锁和非公平锁

    公平锁

    公平锁是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买,后来的人在队尾排着,这是公平的。

    20210606232716-2021-06-06-23-27-17

    在 java 中可以通过构造函数初始化公平锁

    /**
    * 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
    */
    Lock lock = new ReentrantLock(true);
    

    非公平锁

    非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的状态(某个线程一直得不到锁)。

    20210606232737-2021-06-06-23-27-38

    在 java 中 synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁。

    /**
    * 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
    */
    Lock lock = new ReentrantLock(false);
    

    可重入锁

    可重入锁又称之为递归锁,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。

    20210606232755-2021-06-06-23-27-56

    对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁。对于Synchronized而言,也是一个可重入锁。

    敲黑板:可重入锁的一个好处是可一定程度避免死锁。

    以 synchronized 为例,看一下下面的代码:

    public synchronized void mehtodA() throws Exception{
     // Do some magic tings
     mehtodB();
    }
    
    public synchronized void mehtodB() throws Exception{
     // Do some magic tings
    }
    

    上面的代码中 methodA 调用 methodB,如果一个线程调用methodA 已经获取了锁再去调用 methodB 就不需要再次获取锁了,这就是可重入锁的特性。如果不是可重入锁的话,mehtodB 可能不会被当前线程执行,可能造成死锁。

    自旋锁

    自旋锁是指线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环就是所谓的自旋。

    20210606232809-2021-06-06-23-28-09

    自旋锁的目的是为了减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。

    如果锁被另一个线程占用的时间比较长,即使自旋了之后当前线程还是会被挂起,忙循环就会变成浪费系统资源的操作,反而降低了整体性能。因此自旋锁是不适应锁占用时间长的并发情况的。

    在 Java 中,AtomicInteger 类有自旋的操作,我们看一下代码:

    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return v;
    }
    

    CAS 操作如果失败就会一直循环获取当前 value 值然后重试。

    另外自适应自旋锁也需要了解一下。

    在JDK1.6又引入了自适应自旋,这个就比较智能了,自旋时间不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果虚拟机认为这次自旋也很有可能再次成功那就会次序较多的时间,如果自旋很少成功,那以后可能就直接省略掉自旋过程,避免浪费处理器资源。

    分段锁

    分段锁 是一种锁的设计,并不是具体的一种锁。

    分段锁设计目的是将锁的粒度进一步细化,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

    20210606232830-2021-06-06-23-28-31

    在 Java 语言中 CurrentHashMap 底层就用了分段锁,使用Segment,就可以进行并发使用了。

    锁升级(无锁|偏向锁|轻量级锁|重量级锁)

    JDK1.6 为了提升性能减少获得锁和释放锁所带来的消耗,引入了4种锁的状态:无锁偏向锁轻量级锁重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。

    无锁

    无锁状态其实就是上面讲的乐观锁,这里不再赘述。

    偏向锁

    Java偏向锁(Biased Locking)是指它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁。

    偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入。

    轻量级锁

    当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待上一个线程释放锁。

    重量级锁

    如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。

    升级到重量级锁其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态。

    在 Java 中,synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。这一过程在后续讲解 synchronized 关键字的原理时会详细介绍。

    锁优化技术(锁粗化、锁消除)

    锁粗化

    锁粗化就是将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求。

    举个例子,一个循环体中有一个代码同步块,每次循环都会执行加锁解锁操作。

    private static final Object LOCK = new Object();
    
    for(int i = 0;i < 100; i++) {
        synchronized(LOCK){
            // do some magic things
        }
    }
    

    经过锁粗化后就变成下面这个样子了:

     synchronized(LOCK){
         for(int i = 0;i < 100; i++) {
            // do some magic things
        }
    }
    

    锁消除

    锁消除是指虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除。

    举个例子让大家更好理解。

    public String test(String s1, String s2){
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(s1);
        stringBuffer.append(s2);
        return stringBuffer.toString();
    }
    

    上面代码中有一个 test 方法,主要作用是将字符串 s1 和字符串 s2 串联起来。

    test 方法中三个变量s1, s2, stringBuffer, 它们都是局部变量,局部变量是在栈上的,栈是线程私有的,所以就算有多个线程访问 test 方法也是线程安全的。

    我们都知道 StringBuffer 是线程安全的类,append 方法是同步方法,但是 test 方法本来就是线程安全的,为了提升效率,虚拟机帮我们消除了这些同步锁,这个过程就被称为锁消除

    StringBuffer.class
    
    // append 是同步方法
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }
    

    一张图总结:

    Java 并发编程的知识非常多,同时也是 Java 面试的高频考点,面试官必问的,需要学习 Java 并发编程其他知识的小伙伴可以去下载『阿里师兄总结的Java知识笔记 总共 283 页,超级详细』。

    前面讲了 Java 语言中各种各种的锁,最后再通过六个问题统一总结一下:

    Java中那些眼花缭乱的锁-2021-06-16-23-19-40

    大家学会了吗?收藏等于白嫖,点赞才是真爱,雷小帅感谢大家~~~

     

    作者:雷小帅

    推荐一个Github 开源项目,『Java八股文』Java面试套路,Java进阶学习,打破内卷拿大厂Offer,升职加薪!

    作者简介:

    ☕读过几年书:华中科技大学硕士毕业;
    😂浪过几个大厂:华为、网易、百度……
    😘一直坚信技术能改变世界,愿保持初心,加油技术人!

    微信搜索公众号【爱笑的架构师】,关注这个对技术有追求且有趣的打工人。

    展开全文
  • Java多线程 -

    千次阅读 2022-03-28 15:31:49
    指的是线程之间的可见性,一个线程对状态的修改,对其他线程是可见的。在 Java中 volatile、synchronized 和 final 实现可见性。 原子性 如果个操作是不可分割的,我们则称之为原子操作,也就是有原子性。比如i+...

    Java多线程 - 锁

    三性

    • 可见性

      指的是线程之间的可见性,一个线程对状态的修改,对其他线程是可见的。在 Javavolatilesynchronizedfinal 实现可见性。

    • 原子性

      如果一个操作是不可分割的,我们则称之为原子操作,也就是有原子性。比如i++,就不是原子操作。在Javasynchronized和在lockunlock 中操作保证原子性

    • 有序性

      一系列操作是按照规定的顺序发生的。如果在本线程之内观察,所有的操作都是有序的,如果在其他线程观察,所有的操作都是无序的;前半句指“线程内表现为串行语义”后半句指“指令重排序”和“工作内存和主存同步延迟”。Java语言提供了volatilesynchronized 两个关键字来保证线程之间操作的有序性。volatile是因为其本身包含“禁止指令重排序”的语义,
      synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。

    Volatile

    作用

    • 保证了可见性。
    • 防止指令重排序(有序性)。
    • 半个原子性: 对任意单个volatile变量的读/写具有原子性

    读-写时内存语义

    • 写的内存语义

    • 当写一个volatile变量时,JMM会把该线程对应的本地内存中共享变量值刷新会共享内存

    • 读的内存语义

      当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。

    Volatile的内存屏障实现

    是否可以重排序第二个操作第二个操作第二个操作
    第一个操作普通读/写volatile读volatile写
    普通读/写YESYESNO
    volatile读NONONO
    volatile写YESNONO

    编译器生成字节码文件时会在指令序列中插入内存屏障来禁止特定类型的处理器排序。但是,在实际执行过程中,只要不改变volatile的内存语义,
    编译器可以根据实际情况省略部分不必要的内存屏障。

    放置的内存屏障

    在每个volatile写操作前面插入StoreStore屏障
    在每个volatile写操作后面插入StoreLoad屏障
    在每个volatile读操作后面插入LoadLoad屏障
    在每个volatile读操作后面插入LoadStore屏障

    什么是指令重排序

    执行任务的时候,为了提高编译器和处理器的执行性能,编译器和处理器(包括内存系统,内存在行为没有重排但是存储的时候是有变化的)会对指令重排序。编译器优化的重排序是在编译时期完成的,指令重排序和内存重排序是处理器重排序

    编译器优化的重排序

    在不改变单线程语义的情况下重新安排语句的执行顺序。比如,在第10行创建了临时变量,在使用前才进行初始化。或者,下面的情况。

    int a = 3;
    a = 5;
    
    int a = 5
    

    指令级并行重排序

    处理器的指令级并行技术将多条指令重叠执行,如果不存在数据的依赖性,为了使 CPU 内部的运算单元能够尽量被充分利用,处理器可能会对输入的字节码指令进行重排序处理,也就是处理器优化。

    现在的CPU一般采用流水线来执行指令。一个指令的执行被分成:取址、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。指令流水线并不是串行的,多个指令可以同时处于同一个阶段,只要CPU内部相应的处理部件未被占满即可。

    比如说CPU有一个加法器和一个除法器,那么一条加法指令和一条除法指令就可能同时处于“执行”阶段, 而两条加法指令在“执行”阶段就只能串行工作。

    a++; 
    b=f(a);  //会阻塞直到a++有结果
    c--; //没有因果关系,所以,可以先计算。但是,这个先计算是发生在CPU执行时
    

    像这样有依赖关系的指令如果挨得很近,后一条指令必定会因为等待前一条执行的结果,而在流水线中阻塞很久,占用流水线的资源。而编译器的乱序,作为编译优化的一种手段,则试图通过指令重排将这样的两条指令拉开距离, 以至于后一条指令进入CPU的时候,前一条指令结果已经得到了,那么也就不再需要阻塞等待了。比如将指令重排为:

    a++; c--; b=f(a);
    

    相比于CPU的乱序,编译器的乱序才是真正对指令顺序做了调整。但是编译器的乱序也必须保证程序上下文的因果关系不发生改变。

    内存系统的重排序

    因为使用了读写缓存区,使得看起来并不是顺序执行的。

    内存重排序实际上并不是真的相关操作被排序了,而是因为CPU引入高速缓存还没来得及刷新导致;

    寄存器是什么和高速缓存什么区别:

    CPU要取数据,处理数据,都要放到寄存器处理。一般寄存器不用太大,它只要存放指令一次操作的数据就够了。

    高速缓存是内存的部分拷贝,因为高速缓存速度快,把常用的数据放这里可以提高速度。

    每个CPU都有自己的缓存,为了提高共享变量的写操作,CPU把整个操作变成异步的了,如果写入操作还没来的及同步到其它CPU,就有可能发生其它CPU读取到的是旧的值,因此看起来这条指令还没执行一样。

    多CPU架构中,多个CPU高速缓冲区数据同步,依赖于缓存一致性协议

    缓存一致性协议

    在多处理器的情况下,每个处理器总是嗅探总线上传播的数据来检查自己的缓存是否过期,当处理器发现自己对应的缓存对应的地址被修改,
    就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行操作的时候,会重新从系统中把数据督导处理器的缓存里。这个协议被称之为缓存一致性协议。

    CPU对于Volatile的支持

    在有volatile修饰的共享变量进行写操作的时候会多出一条带有lock前缀的汇编代码,而这个lock操作会做两件事:

    • 将当前处理器的缓存行的数据同步到系统内存。lock信号确保存在该信号期间CPU可以独占共享内存。在之前通过锁总线的方式,现在采用锁缓存的方式。
    • 这个写回操作会使其他处理器的缓存中缓存了该地址的缓存行无效。在下一次这些CPU需要使用这些地址的值时,强制要求去共享内存中读取。

    总结

    volatile提供了一种轻量级同步机制来完成同步,它可以保操作的可见性、有序性以及对于单个volatile变量的读/写具有原子性,对于符合操作等非原子操作不具有原子性。

    volatile通过添加内存屏障(特殊的字节码)及缓存一致性协议(lock指令)来完成对可见性的保证。

    Synchronized

    三种应用方式

    • 修饰实例方法,作用于当前实例(this)加锁,进入同步代码前要获得当前实例的锁

    • 修饰静态方法,作用于当前类对象(XX.class)加锁,进入同步代码前要获得当前类对象的锁

    • 修饰代码块,指定加锁对象,对给定对象(object)加锁,进入同步代码库前要获得给定对象的锁。

    Synchronized 锁升级

    在Java SE1.6里锁一共有四种状态,无锁状态偏向锁状态轻量级锁状态重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

    升级过程

    1. 一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
    2. 一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了。检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程。如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁;如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
    3. 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

    其中,偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。

    Synchronized底层原理

    Java对象结构

    Java对象结构

    • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
    • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。(相当于C语言的内存对其)
    • 对象头:jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成。

    Java对象头结构

    虚拟机位数头对象结构说明
    32/64bitMark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息
    32/64bitClass Metadata Address类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

    Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构

    锁状态25bit4bit1bit是否是偏向锁2bit 锁标志位
    无锁状态对象HashCode对象分代年龄001

    由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:
    Mark Word特殊结构

    加上最初的默认状态,一共有5种变化。

    重量级锁

    Monitor对象

    Synchronized为重量级锁时,指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其monitor之间的关系有存在多种实现方式。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的。

    JVM: 即Java Virtual Machine,JAVA编译后生成的JAVA字节码是在JVM上跑,须要由JVM把字节码翻译成机器指令。主流的JVM包括Hotspot、Jikes RVM等。JVM运行的码文件是.jar

    DVM:即Dalvik Virtual Machine,是安卓中使用的虚拟机。每一个进程相应着一个Dalvik虚拟机实例。Dalvik虚拟机运行的是Dalvik字节码。Dalvik使用即时编译(JIT)。因为JVM是Oracle公司(原SUN公司)的产品,Google担心版权的问题,既然Java是开源的,索性就研究了JVM,写出了DVM。Android执行的码文件是.dex

    ART:Android最新的虚拟机,使用AOT技术。

    JIT:JIT会在运行时分析应用程序的代码,识别哪些方法可以归类为热方法,这些方法会被JIT编译器编译成对应的汇编代码,然后存储到代码缓存中,以后调用这些方法时就不用解释执行了,可以直接使用代码缓存中已编译好的汇编代码。

    AOT:预编译。在运行之前,就对其包含的Dex字节码进行翻译,得到对应的本地机器指令,于是就可以在运行时直接执行了。

    ObjectMonitor的主要数据结构如下:

    ObjectMonitor() {
        _header       = NULL;
        _count        = 0; //记录个数
        _waiters      = 0,
        _recursions   = 0;
        _object       = NULL;
        _owner        = NULL; //指向持有ObjectMonitor对象的线程
        _WaitSet      = NULL; //保存ObjectWaiter对象列表.处于wait状态的线程,会被加入到_WaitSet
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ; //保存ObjectWaiter对象列表.处于等待锁block状态的线程,会被加入到该列表
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
      }
    

    每个等待锁的线程都会被封装成ObjectWaiter对象。

    当多个线程同时访问一段同步代码时:

    1. 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
    2. 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
    3. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);

    总结:monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。

    Synchronized重量级锁底层原理

    对于Synchronized代码块,在字节码中使用monitorentermonitorexit实现。

    3: monitorenter  //进入同步代码
    //..........省略其他  
    15: monitorexit   //退出同步代码
    16: goto          24 //正常退出,继续执行
    //省略其他.......
    21: monitorexit //异常结束时被执行的释放monitor 的指令
    

    偏向锁

    HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁是为了在只有一个线程执行同步块时提高性能。

    优势

    当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。

    流程
    1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
    2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)
    3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
    4. 如果CAS获取偏向锁失败,则表示有竞争(CAS获取偏向锁失败说明至少有过其他线程曾经获得过偏向锁,因为线程不会主动去释放偏向锁)。当到达全局安全点(safepoint)时,会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着(因为可能持有偏向锁的线程已经执行完毕,但是该线程并不会主动去释放偏向锁),如果线程不处于活动状态,则将对象头设置成无锁状态(标志位为“01”),然后重新偏向新的线程;如果线程仍然活着,撤销偏向锁后升级到轻量级锁状态(标志位为“00”),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
    5. 执行同步代码
    总结

    偏向锁只适用于一个线程竞争获得锁的情况,如果出现另一个线程竞争,立即升级为轻量级锁

    轻量级锁

    轻量级锁是为了在线程近乎交替执行同步块时提高性能。

    流程
    1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如下图所示。
      锁记录
    2. 拷贝对象头中的Mark Word复制到锁记录中。
    3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。
    4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如下图所示。
      轻量级锁定状态内存图
    5. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,若当前只有一个等待线程,则可通过自旋稍微等待一下,可能另一个线程很快就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

    Synchronized锁类型

    • synchronized 锁是非公平锁。

    • synchronized是可重入锁,所以不会自己把自己锁死

    Java锁

    CAS操作

    使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。

    CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。

    因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用**CAS(compare and swap)**又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

    操作过程

    CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:

    • V 内存地址存放的实际值
    • O 预期的值(旧值)
    • N 更新的新值。

    更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

    举例:

    1.在内存地址V当中,存储着值为10的变量。

    2.此时线程1想把变量的值增加1.对线程1来说,旧的预期值A=10,要修改的新值B=11。

    3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

    4.线程1开始提交更新,首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败。

    5.线程1 重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

    6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的。

    7.线程1进行交换,把地址V的值替换为B,也就是12.

    Java锁的种类

    公平锁/非公平锁

    • 公平锁:加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程,先来先得。

    • 非公平锁:线程加锁时直接尝试获取锁,获取不到就自动到队尾等待。

    可重入锁/不可重入锁

    • 可重入锁:当线程获取某个锁后,还可以继续获取它,可以递归调用,而不会发生死锁

    Java的可重入锁有:reentrantLock(显式的可重入锁)、synchronized(隐式的可重入锁)

    • 不可重入锁:与可重入相反,获取锁后不能重复获取,否则会死锁(自己锁自己)。

    独享锁/共享锁

    • 独占锁:指该锁一次只能被一个线程所持有。

    对ReentrantLock和Synchronized而言都是独占锁

    • 共享锁:指该锁可以被多个线程锁持有。

    对ReentrantReadWriteLock其读锁是共享,其写锁是独占写的时候只能一个人写,但是读的时候,可以多个人同时读

    互斥锁/读写锁

    互斥锁

    在访问共享资源之前对其进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。

    如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源。举个形象的例子:多个人抢一个马桶。

    读写锁

    读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。

    读写锁有三种状态 :读加锁状态、写加锁状态和不加锁状态

    乐观锁/悲观锁

    乐观锁

    总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐 观锁适用于多读的应用类型,这样可以提高吞吐量 。

    悲观锁

    总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁( 共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程 )。

    分段锁

    分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

    并发容器类的加锁机制是基于粒度更小的分段锁,分段锁也是提升多并发程序性能的重要手段之一。

    容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

    在ConcurrentHashMap中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设使用合理的散列算法使关键字能够均匀的分部,那么这大约能使对锁的请求减少到越来的1/16。也正是这项技术使得ConcurrentHashMap支持多达16个并发的写入线程。

    自旋锁

    自旋锁,就是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。

    自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了CPU处理器的时间

    如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,这样反而会带来性能上的浪费。

    展开全文
  •  首先我这边是实际开发中使用到了,开发的环境情况为:有一个数据中心(暂且当做一个Map集合),有两线程A、B,其中A线程每5秒钟从其他地方获取到新来的数据然后和数据中心里面的数据进行一个融合,然后B线程...
  • 个线程run方法里面使用同一个request,然后通过request.getParameter()获取入参。次查询总有那么1,2个线程拿不到参数,跑出空指针异常。我代码排查到如下类似代码,可以复现问题。使用postman模拟请求...
  • Java多线程|同步与机制

    千次阅读 2016-06-03 20:06:43
    总结 Java「与同步」机制。
  • 多线程一线程1.普通方法调用和多线程2.程序、进行、线程二、线程创建1.继承Thread类2.实现Runable接口3.实现Callable接口4.静态代理模式5、Lamda表达式三、线程状态1.线程状态2.线程方法3.线程停止4.线程休眠sleep...
  • Python中线程同步与线程锁

    千次阅读 多人点赞 2019-06-09 21:12:02
    文章目录Python中线程同步与线程锁线程同步threading.Event对象threading.Timer定时器,延迟执行threading.Lock可重入RLockCondition条件,等待通知therading.Semaphore信号量threading.BoundedSemaphore有界...
  • JUC多线程:synchronized机制原理 与 Lock机制

    万次阅读 多人点赞 2021-08-26 08:53:31
    synchronized 通过当前线程持有对象,从而拥有访问权限,而其他没有持有当前对象的线程无法拥有访问权限,保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块,从而保证线程安全。synchronized 机制...
  • 多线程安全——synchronized升级

    千次阅读 2021-11-18 15:46:08
    而操作系统实现线程之间的切换时需要从用户态转换到内核态,有可能给线程加锁消耗的时间比有用操作消耗的时间更。 Java1.6后对synchronized 进行了很的优化,有适应自旋、消除、粗化、轻量级及偏向等,...
  • 多个任务之间有依赖关系,某个任务的运行依赖于另个任务。 【同步】:   是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。...
  • 多线程之间如何实现同步?

    万次阅读 2019-07-01 11:56:53
    线程安全问题一般是发生再多线程环境,当多个线程同时共享个全局变量或静态变量做写的操作时候,可能会发生数据冲突问题,也就是线程安全问题,在读的操作不会发生数据冲突问题 下面看个简单的买票例子 案例:需求...
  • Java多线程安全问题和

    千次阅读 2020-02-28 17:32:26
    多个线程同时操作同一个数据是,可能会出现数据不一样的情况,这就是线程安全问题。 线程安全机制用于保证多个线程访问数据时的一致性. 线程安全问题体现在三个方面: 1、原子性 2、可见性 3、有序性 原子操作是不...
  • 【Java多线程-6】synchronized同步

    千次阅读 2020-03-31 15:35:49
    前文描述了Java多线程编程,多线程的方式提高了系统资源利用和程序效率,但多个线程同时处理共享的数据时,就将面临线程安全的问题。 例如,下面模拟这样个场景:个售票处有3个售票员,出售20张票。 public ...
  • 线程的同步是为了防止多个线程访问个数据对象时,对数据造成的破坏。 例如:两个线程ThreadA、ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据。   public class Foo {   private int...
  • 最近在做一个小功能,具体逻辑是通过代码读取datahub数据并把获取到的数据产出成数据文件。因为数据量较大,所以考虑到使用多线程来运行。下面是主要代码 线程启动方法 public void start() throws Exception { ...
  • python的多线程线程同步方式

    千次阅读 2019-07-17 09:37:23
    1.线程执行 join与setDaemon 1.子线程在主线程运行结束后,会继续执行完,如果给子线程设置为守护线程(setDaemon=True),主线程运行结束子线程即结束; 2 .如果join()线程,那么主线程会等待子线程执行完再执行...
  • C# 多线程如何访问同一个数据

    万次阅读 2016-09-07 14:18:21
    多线程中,为了使数据保持一致性必须要对数据或是访问数据的函数加锁,在数据库中这是很常见的,但是在程序中大部分都是单线程的程序,所以没有加锁的必要,但是在多线程中,为了保持数据的同步,一定要加锁。...
  • Java多线程,Android多线程

    千次阅读 2022-03-12 08:10:27
    2,实现Runnable接口,特点:灵活,方便同一个对象被多个线程使用 3,实现Callable接口 静态代理模式 真实对象和代理对象药实现同一个接口,代理对象要代理真实角色; 代理对象可做很多真实对象做不了的事情,...
  • C#中支持多线程,而多线程引发的个比较突出的问题就是在同一个时间里,可能会有多个线程访问同一个资源,引起资源的竞争 导致数据损坏。 lock 关键字可以用来确保代码块完成运行,而不会被其他线程中断。这是通过...
  • 多线程面试题和答案:线程锁+线程池+线程同步1、并发编程三要素?2、多线程的价值?3、创建线程的有哪些方式?区别是什么?4、创建线程的三种方式的对比?4、线程的生命周期及五种基本状态及转换条件1、Java线程具有...
  • java多线程的15种

    万次阅读 多人点赞 2019-05-05 19:19:24
    悲观认为自己在使用数据的时候,一定有别的线程来修改数据,在获取数据的时候会先加锁,确保数据不会被别的线程修改。 实现:关键字synchronized、接口Lock的实现类 使用的场景:写操作较,先加锁可以保证写...
  • Java多线程面试题

    万次阅读 多人点赞 2020-10-25 15:56:40
    sleep 方法: 是 Thread 类的静态方法,当前线程将睡眠 n 毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进行可运行状态,等待 CPU 的到来。睡眠不释放(如果有的话); wait 方法: 是 Object 的方法...
  • 而且任何一对象都能作为一把内置。在`JDK1.4`及之前就是使用`内置Synchronized`来进行线程同步控制的** `上文说,任何一对象都能作为一把内置”,意味着synchronized关键字出现的地方,都有一对象与之...
  • 【Java多线程】停止线程

    万次阅读 2021-10-03 22:37:21
    目录 1. 停止线程的方法 2. 判断线程是否是停止状态 3. 能停止的线程——异常法 4. 在沉睡中停止 5. 能停止的线程——暴力停止...2)使用stop方法强行终止线程,但是不推荐使用这方法,因为stop和suspend及resum
  • 死锁是多线程中最差的种情况,多个线程相互占用对方的资源的,而又相互等 对方释放,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁。 举个例子,A 同学抢了 B 同学的钢笔,B 同学...
  • MFC多线程互斥的使用

    千次阅读 2019-10-04 23:14:12
    MFC线程互斥的使用 本例演示在MFC中使用线程。第部分实现线程的开启、暂停、继续、注销(见上篇...1.在原有基础上(见上篇文章MFC线程的开启、暂停、继续和注销)添加个EDIT控件和一个线程MyThrea...
  • 简述python多线程中的互斥

    千次阅读 2018-05-04 10:13:03
    的标记,这个标记用来保证在任时刻,只能有一个线程访问该对象。在python中由于线程的存在,并且对全局变量作用时有可能产生全局变量紊乱问题,所以也加入了同步和互斥等方法,但是我们主要讲解互斥:如下...
  • 有序性编程要求题第2关:使用synchronized关键字同步线程任务描述相关知识并发编程什么时候会出现安全问题如何解决线程安全问题synchronized关键字synchronized代码块编程要求测试说明代码示例第3关:使用线程锁...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 258,361
精华内容 103,344
关键字:

多个线程获取同一把锁