线程安全
1:多个线程共同访问一个对象上的方法内部的私有变量(局部变量),方法中的变量不存在 非线程安全问题,永远是线程安全的。这是因为方法内部的变量是私有的特性造成的。
实例变量非线程安全:
2:多个线程共同访问一个对象的实例变量(成员变量),
多个线程共同访问一个对象的中的同步方法时一定是线程安全的。 方法前加synchronized关键字
3:多个线程访问多个锁:
*两个线程分别访问同一个类(同一个对象的相同名称的同步方法)的两个不同实例的(创建两个)相同名称的同步方法,效果是以异步的方式运行的
两个线程分别访问两个对象的同一个的同步方法进行访问。
*关键字synchronized取得的锁是对象锁,而不是一段代码或者方法当作锁,那个线程先执行带synchronized关键字的方法,
*那个线程就持有该方法所属对象的锁Lock,那么其他线程只能持等待状态,前提是多个线程访问的是同一个对象。
*但是如果多个线程访问多个对象,则JVM会创建多个锁。
4:synchronized方法与锁对象
调用用关键字synchronized声明的方法一定是排队执行的。只有共享资源的读写访问才需要同步化。
两个线程分别访问同一个对象的的两个方法(一个同步,一个不同步),
线程A先持有Object对象的锁,线程B完全可以异步调用非synchronized类型的方法。
两个线程分别访问同一个对象的的两个的同步方法,
线程A先持有Object对象的Lock锁,线程B如果在这时调用Object对象中的synchronized类型的方法则需等待,也就是同步。
一段
synchronized
的代码被一个线程执行之前,他要先拿到执行这段代码的权限,
在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁);
如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池等待队列中)。
取到锁后,他就开始执行同步代码(被
synchronized
修饰的代码);
线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码了。
事实上,synchronized修饰非静态方法、同步代码块的synchronized (this)用法和synchronized (非this对象)的用法锁的是对象,线程想要执行对应同步代码,需要获得对象锁。
synchronized修饰静态方法以及同步代码块的synchronized (类.class)用法锁的是类,线程想要执行对应同步代码,需要获得类锁。
私有锁:在类内部声明一个私有属性如private Object lock,在需要加锁的代码段synchronized(lock)。
回顾基础知识,Java中的每一个对象都可以作为锁,而不同的场景锁是不一样的。
- 对于实例同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前对象的Class对象。
- 对于同步方法块,锁是Synchonized括号里配置的对象
5:脏读:
多个线程在读取实例变量时,赋值时进行了同步,但在取值时其值已经被其他线程更改过了。
解决方案:getValue()和setValue()方法上加synchronized关键字。
6: synchronized锁重入
关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。
可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。
以下来自博客 https://www.cnblogs.com/cielosun/p/6684775.html
1.2. 可重入
1.2.1. 定义
关于可重入这一概念,我们需要参考维基百科。
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
1.2.2. 可重入的条件
- 不在函数内使用静态或全局数据。
- 不返回静态或全局数据,所有数据都由函数的调用者提供。
- 使用本地数据(工作内存),或者通过制作全局数据的本地拷贝来保护全局数据。
- 不调用不可重入函数。
1.3. 可重入与线程安全
一般而言,可重入的函数一定是线程安全的,反之则不一定成立。在不加锁的前提下,如果一个函数用到了全局或静态变量,那么它不是线程安全的,也不是可重入的。如果我们加以改进,对全局变量的访问加锁,此时它是线程安全的但不是可重入的,因为通常的枷锁方式是针对不同线程的访问(如Java的synchronized),当同一个线程多次访问就会出现问题。只有当函数满足可重入的四条条件时,才是可重入的。
2. synchronized的可重入性
2.1. synchronized是可重入锁
回到引言里的问题,如果一个获取锁的线程调用其它的synchronized修饰的方法,会发生什么?
从设计上讲,当一个线程请求一个由其他线程持有的对象锁时,该线程会阻塞。当线程请求自己持有的对象锁时,如果该线程是重入锁,请求就会成功,否则阻塞。
我们回来看synchronized,synchronized拥有强制原子性的内部锁机制,是一个可重入锁。因此,在一个线程使用synchronized方法时调用该对象另一个synchronized方法,即一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的。
在Java内部,同一个线程调用自己类中其他synchronized方法/块时不会阻碍该线程的执行,同一个线程对同一个对象锁是可重入的,同一个线程可以获取同一把锁多次,也就是可以多次重入。原因是Java中线程获得对象锁的操作是以线程为单位的,而不是以调用为单位的。
2.2. synchronized可重入锁的实现
之前谈到过,每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。
关于父类和子类的锁的重入:
子类完全可以父类的synchonized方法,然后调用父类中的方法,
子类完全可以通过"可重入锁"调用父类的同步方法,
以下解释来自博客 http://blog.csdn.net/aitangyong/article/details/22695399
*使用A a = new A()这种方式创建对象的时候,JVM会在后台给我们分配内存空间,然后调用构造函数执行初始化操作,*最后返回内存空间的引用。
*即构造函数只是进行初始化,并不负责分配内存空间(创建对象)。
*所以呢其实创建子类对象的时候,JVM会为子类对象分配内存空间,
*并调用父类的构造函数。
*我们可以这样理解:创建了一个子类对象的时候,在子类对象内存中,
*有两份数据,一份继承自父类,一份来自子类,但是他们属于同一个对象(子类对象),
*只不过是java语法提供了this和super关键字来让我们能够按照需要访问这2份数据而已。
*这样就产生了子类和父类的概念,但实际上只有子类对象,没有父类对象。
可重入锁最大的作用是避免死锁
7:锁的异常
当一个线程执行的代码出现异常,其所持有的锁会自动释放。
8:同步不具有基础性
父类同步,子类不能继承父类的同步,子类需在方法中添加synchronized关键字
synchronized和lock的区别:
Lock 的锁定是通过代码实现的,而 synchronized 是在 JVM 层面上实现的。
synchronized 在锁定时如果方法块抛出异常,JVM 会自动将锁释放掉,不会因为出了异常没有释放锁造成线程死锁。但是 Lock 的话就享受不到 JVM 带来自动的功能,出现异常时必须在 finally 将锁释放掉,否则将会引起死锁。
在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好。