【非线程安全】其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是【脏读】,也就是取到的数据其实是被更改过的,而【线程安全】就是以获得的实例变量的值是经过同步处理的,不会出现脏读的现象。
非线程安全问题存在于【实例变量】中,如果是方法内部的私有变量,则不存在非线程安全问题,所得结果也就是线程安全的了。
方法中的变量不存在非线程安全的问题,永远都是线程安全的,这时方法内部的变量是私有的特性造成的。
如果是多个线程共同访问 1 个对象中的实例变量,则有可能出现【非线程安全】问题。
为了解决非线程安全问题,只要在方法前加关键字 synchronized 即可。
关键字 synchronized 取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,哪个线程先执行带 synchronized 关键字的方法,哪个线程就持有该方法所属对象的锁 Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。
但如果多个线程访问多个对象,则 JVM 会创建多个锁。
同步的单词为 synchronized,异步的单词为 asynchronized。
调用关键字 synchronized 声明的方法一定是排队运行的,另外,只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本就没有同步的必要。
两个线程 A 和 B:
发生脏读的情况就是在读取实例变量时,此自值已经被其他线程更改过了。
当 A 线程调用 anyObject 对象加入 synchronized 关键字的 X 方法时,A 线程就获得了 X 方法锁,更准确的讲,是获得了对象的锁,所以其他线程必须等 A 线程执行完毕后才可以调用 X 方法,但 B 线程可以随意调用其他非 synchronized 同步方法。
当 A 线程调用 anyObject 对象加入 synchronized 关键字的 X 方法时,A 线程就获得了 X 方法所在的对象的锁,所以其他线程必须等 A 线程执行完毕后才可以调用 X 方法,而 B 线程如果调用声明了 synchronized 关键字的非 X 方法时,必须等 A 线程将 X 方法执行完毕后,也就是释放对象锁后才可以调用,这时 A 线程已经执行了一个完整的任务,故不存在脏读的情况。
脏读一定会出现操作实例变量的情况,这就是不同线程【争抢】实例变量的结果。
【可重入锁】的概念是:自己可以再次获取自己的内部锁。
比如有 1 个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这俄格对象的锁的时候,还是可以获取的,如果不可锁重入的话,就会造成死锁。
关键字 synchronized 拥有锁重入的功能,也就是在使用 synchronized 时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个 synchronized 方法/块的内部调用本类的其他 synchronized 方法/块时,是永远可以得到锁的。
当存在父子类继承关系时,子类是完全可以通过【可重入锁】调用父类的同步方法的。
当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
同步不能继承,还得在子类的方法中添加 synchronized 关键字。
用关键字 synchronized 声明方法在某些情况下是有弊端的,比如 A 线程调用同步方法执行一个长时间的任务,那么 B 线程必须等待比较长的时间,在这样的情况下可以使用 synchronized 同步语句块来解决。
比如 A 线程调用同步方法执行一个长时间的任务,那么 B 线程必须等待比较长的时间,耗时较长。
当两个并发线程访问同一个对象 object 中的 synchronized 同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完以后才能执行该代码块。
不在 synchronized 块中就是异步执行,在 synchronized 块中就是同步执行。
当一个线程访问 object 的一个 synchronized 同步代码块时,其他线程对同一个 object 中所有其他 synchronized(this) 同步代码块的访问将被阻塞,这说明 synchronized 使用的对象监视器是同一个。
和 synchronized 方法一样,synchronized(this) 代码块也是锁定当前对象的。
多个线程调用同一个对象中的不同名称的 synchronized 同步方法或 synchronized(this) 同步代码块时,调用的效果就是按顺序执行,也就是同步的,阻塞的。
这说明 synchronized 同步方法或 synchronized(this) 同步代码块分别有两种作用。
锁定 this 对象具有一定的有点:如果一个类中有很多个 synchronized 方法,这时虽然能实现同步,但会受到阻塞,所以影响效率;但如果使用同步代码块锁非 this 对象,则 synchronized(非 this) 代码块中的程序与同步方法是异步的,不与其他锁 this 同步方法争抢 this 锁,则可大大提高运行效率。
使用 synchronized(非 this 对象 x) 同步代码块格式进行操作时,对象监视器必须是同一个对象。如果不是同一个对象监视器,运行的结果就是异步调用了,就会交叉运行。
同步代码块放在非同步 synchronized 方法中进行声明,并不能保证调用方法的线程的执行同步/顺序性,也就是线程调用方法的顺序是无序的,虽然在同步代码块中执行的顺序是同步的这样极易出现【脏读】问题。
但需要注意:如果其他线程调用不加 synchronized 关键字的方法时,还是异步调用。
关键字 synchronized 还可以应用在 static 静态方法上,如果这样写,*那是对当前的 .java 文件对应的 Class 类进行持锁,Class 锁可以对类的所有对象实例起作用(即使是两个不同对象)。
在大多数情况下,同步 synchronized 代码块都不能使用 String 作为锁对象,而改用其他,比如 new Object() 实例化一个 Object 对象,但不把他放入缓存中。
同步方法容易造成死循环,因为同步方法锁住的是实例对象,其他线程无法获取锁对象时,就无法执行方法。
…
…
关键字 volatile 的主要作用是使变量在多个线程间可见。
…
对变量赋 volatile 关键字,可以使当前线程访问变量时,强制性地从公共堆栈中进行取值。
使用 volatile 关键字增加了实例变量在多个线程之间的可见性。但 volatile 关键字最致命的是不支持原子性。
synchronized 与 volatile 比较:
关键字 volatile 虽然增加了实例变量在多个线程之间的可见性,但它却不具备同步性,那么也不具备原子性。
关键字 volatile 主要使用的场合是在多个线程中可以感知实例变量被更改了,并且可以获取最新的值使用,也就是用多线程读取共享变量时可以获得最新值使用。
关键字 volatile 提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。
表达式 i++ 的操作步骤:
变量在内存中的工作过程:
在多线程环境中,use 和 assign 是多次出现的,但这一操作并不是原子性的,也就是在 read 和 load 之后,如果主内存 count 变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和公共内存中的变量不同步,所以计算出来的结果会和预期不一样,也就出现了非线程安全问题。
对于用 volatile 修饰的变量,JVM 虚拟机只是保证从主内存加载到线程工作内存的值是最新的,例如线程 1 和 线程 2 在进行 read 和 load 的操作时,发现主内存中 count 的值都是 5,那么都会加载这个最新的值。也就是说,volatile 关键字解决的是变量读时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。
除了在 i++ 操作时使用 synchronized 关键字实现同步外,还可以使用 AtomicInteger 原子类实现。
原子操作是一个不能分割的操作,没有其他线程能够中断或检查正在原子操作中的变量。
private AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
原子类在具有有逻辑性的情况下输出结果也具有随机性。
原因在于:addAndGet( ) 方法是原子性的,但方法和方法之间的调用却不是原子性的,这样的问题必须要用同步。
关键字 synchronized 可以使多个线程访问同一个资源具有同步性,而且它还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能。
关键字 synchronized 可以保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特征:互斥性和可见性。同步 synchronized 不仅可以解决一个线程看到对象处于不一致的状态,还可以保证同步方法或者代码块的每个线程,都能看到由同一个锁保护之前所有的修改效果。
学习多线程并发,要注重【外练互斥,内修可见】,这是掌握多线程、学习多线程并发的重要技术点。