目录
5、静态同步Synchronized方法和Synchronized(class)代码块,类锁
5.1静态Synchronized方法和非静态Synchronized方法的区别
一、变量的线程安全问题
1、方法内的变量为线程安全
java在运行时,数据区、虚拟机 栈或者本地方法栈(执行方法所在的内存区域)是线程私有的,每个线程都有自己的虚拟机栈、存储区和本地方法栈,所以线程在执行方法时,方法内部的变量不会牵扯到共享这一原则,只有共享内存才会牵扯到线程安全问题,方法外变量的内存在堆。
2、方法外变量为非线程安全
脏读一定会出现在操作实例变量(方法外变量)的情况下,是由不同线程争抢实例变量而引发的非线程安全问题。
二、Synchronized方法和锁对象
1、多个对象多个锁
两个线程分别访问同一个类的两个不同实例的相同名称的Synchronized同步方法,执行是异步的。这是因为Synchronized取得的锁都是对象锁,多个对象就具有多个锁,互不干涉。因此要使线程同步,前提是多个线程访问的是同一个对象。
2、Synchronized修饰方法(锁为对象锁)
(1)创建MyService对象
public class MyService {
// synchronized修饰的同步方法
public synchronized void serviceA(){
try {
System.out.println(Thread.currentThread().getName()+"serviceA方法开始...");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+"serviceA方法结束...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void serviceB(){
try {
System.out.println(Thread.currentThread().getName()+"serviceB方法开始...");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+"serviceB方法结束...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
(2)创建执行线程A和B
线程A
public class MyThreadA extends Thread {
public MyService myService;
public MyThreadA(MyService myService){
super();
this.myService = myService;
}
@Override
public void run() {
myService.serviceA();
}
public static void main(String[] args) throws InterruptedException {
MyService myService = new MyService();
MyThreadA myThreadA = new MyThreadA(myService);
myThreadA.setName("A线程");
MyThreadB myThreadB = new MyThreadB(myService);
myThreadB.setName("B线程");
myThreadA.start();
myThreadB.start();
}
}
线程B
public class MyThreadB extends Thread {
public MyService myService;
public MyThreadB(MyService myService){
super();
this.myService = myService;
}
@Override
public void run() {
myService.serviceB();
}
}
执行结果:
实验结果1:A线程先持有MyService对象锁,B线程可以以异步的方式调用MyService对象中的非Synchronized方法。
此时修改MyService对象中serviceB方法为同步方法
public synchronized void serviceB(){
// 此时方法B也设为同步
try {
System.out.println(Thread.currentThread().getName()+"serviceB方法开始...");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+"serviceB方法结束...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
执行结果:
实验结果2:A线程先持有MyService对象锁,B线程这时在调用MyService对象中的Synchronized类型的方法时需要等待,也就是按顺序同步执行。
3、Synchronized锁重入和同步不具备继承性
(1)重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。可重入锁的作用是在一定程度上避免死锁。
测试代码:
创建MyService对象
public class MyService {
public int i = 10;
public synchronized void service() {
try {
i--;
System.out.println("父类操作数:i=" + i);
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
创建MyService的子对象
public class Child_Service extends MyService {
public synchronized void childService() {
try {
while (i > 0) {
// 子类和父类同时操作父类中的变量i,此时子类已经持有了子类的对象锁
i--;
System.out.println("子类操作数:i=" + i);
Thread.sleep(100);
// 调用父类方法,但此时依然只是持有子类对象的锁(第二次获取锁)
this.service();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
创建执行线程
public class MyThread extends Thread {
@Override
public void run() {
Child_Service child_service = new Child_Service();
child_service.childService();
}
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
}
}
打印结果
从上述结果看出,父类和子类依次交替打印,这说明锁是可以重入的,如果锁不能重入的话,父类方法根本就不会执行,也就是说,在持有一个锁的情况下,可以再次获取这个锁,这变是锁的重入性。
通俗来说:当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。
同一线程在调用自己类中其他 synchronized 方法/块或调用父类的 synchronized 方法/块都不会阻碍该线程的执行。就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。
重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
(2)同步不具备继承性
子类重写父类Synchronized方法时,如果不在方法前再次使用Synchronized进行修饰,此方法不具备同步性。
4、Synchronized同步语句块,任一对象监视器
(1)使用同步代码块的好处是,把只有需要同步的代码进行同步,提高程序运行效率。
(2)Synchronized(this)代码块中的锁,锁的是当前对象,也就是对象锁。这是因为使用了this作为“对象监视器”,此时的this指定的是当前对象。
(3)Synchronized(非this)使用优点,如果在一个类中有很多个Synchronized方法,方法与方法之间并不存在同步关系,那么就可以使用Synchronized(非this),使得每一个方法都拥有自己锁,进而提高程序运行效率。
Synchronized(非this)使得锁的控制更加的细化,Synchronized(this)锁的是整个对象,那么Synchronized(非this)可以细化到具体方法里边。
java中可以使用“任一对象”作为“对象监视器”来实现同步功能。这个“任一变量”大多数是实例变量或者方法参数。
代码示例:
创建MyService类,其中一个方法为对象监视器(this),另一个方法为非this监视器
public class MyService {
private String anyString = new String();
public void a(){
try {
// 此锁非this对象锁
synchronized (anyString){
System.out.println(Thread.currentThread().getName()+":进入方法A...");
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName()+":方法A结束...");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void b(){
// this对象锁
synchronized (this) {
System.out.println(Thread.currentThread().getName()+":方法B开始...");
System.out.println(Thread.currentThread().getName()+":方法B结束...");
}
}
}
创建线程——线程A:
public class ThreadA extends Thread {
private MyService myService;
public ThreadA(MyService myService){
super();
this.myService = myService;
}
public void run(){
myService.a();
}
public static void main(String[] args) {
// 代码执行如下
MyService myService = new MyService();
ThreadA threadA = new ThreadA(myService);
ThreadB threadB = new ThreadB(myService);
threadA.start();
threadB.start();
}
}
创建线程——线程B:
public class ThreadB extends Thread {
private MyService myService;
public ThreadB(MyService myService){
super();
this.myService = myService;
}
public void run(){
myService.b();
}
}
执行结果:
由于对象监视器不同,所以运行结果就是异步的。
4.1数据类型String的常量池特性
在JVM中具有String常量池缓存的功能,所以使用String作为对象监视器时,需要注意此问题
想了解常量池特性?请看这篇文章:
测试代码:
创建MyService类
public class MyService {
public void method(String str){
try {
synchronized (str) {
System.out.println(Thread.currentThread().getName()+":静态方法A开始...");
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName()+":静态方法A结束...");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
创建线程——A线程
public class ThreadA extends Thread {
private MyService myService;
public ThreadA(MyService myService){
super();
this.myService = myService;
}
public void run(){
myService.method("str");
}
public static void main(String[] args) {
MyService myService = new MyService();
ThreadA threadA = new ThreadA(myService);
ThreadB threadB = new ThreadB(myService);
threadA.start();
threadB.start();
}
}
创建线程——B线程
public class ThreadB extends Thread {
private MyService myService;
public ThreadB(MyService myService){
super();
this.myService = myService;
}
public void run(){
myService.method("str");
}
}
执行结果:
以上结果说明,两个线程持有的是同一把锁,它们的对象监视器都是“str”,为同一监视器。
为什么会出现这种情况呢?明明是分两次传进去的,接下来看一个小测试
public static void main(String[] args) {
String strA = "string";
String strB = "string";
String strC = new String("string");
String strD = new String("string");
System.out.println("字符串A、B相等吗?"+(strA == strB));
System.out.println("字符串C、D相等吗?"+(strC == strD));
}
输出结果:
说明在常量池中创建的对象是同一个对象,而使用new关键字创建的变量是重新分配内存的,是两个不同的对象。
5、静态同步Synchronized方法和Synchronized(class)代码块,类锁
关键字Synchronized应用在静态方法上,对Class类进行持锁,类锁和对象锁是两种不同的锁。
测试代码:
创建MyService类:
public class MyService {
/**
* 静态方法锁:class锁
*/
public synchronized static void a(){
try {
System.out.println(Thread.currentThread().getName()+":静态方法A开始...");
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName()+":静态方法A结束...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 静态方法锁:class锁
*/
public synchronized static void b(){
System.out.println(Thread.currentThread().getName()+":静态方法B开始...");
System.out.println(Thread.currentThread().getName()+":静态方法B结束...");
}
/**
* 非静态方法锁:对象锁
*/
public synchronized void c(){
System.out.println(Thread.currentThread().getName()+":方法C开始...");
System.out.println(Thread.currentThread().getName()+":方法C结束...");
}
}
创建线程——A线程:
public class ThreadA extends Thread {
private MyService myService;
public ThreadA(MyService myService){
super();
this.myService = myService;
}
public void run(){
myService.a();
}
public static void main(String[] args) {
// 执行代码
MyService myService = new MyService();
ThreadA threadA = new ThreadA(myService);
ThreadB threadB = new ThreadB(myService);
ThreadC threadC = new ThreadC(myService);
threadA.start();
threadB.start();
threadC.start();
}
}
创建线程——B线程:
public class ThreadB extends Thread {
private MyService myService;
public ThreadB(MyService myService){
super();
this.myService = myService;
}
public void run(){
myService.b();
}
}
创建线程——C线程:
public class ThreadC extends Thread {
private MyService myService;
public ThreadC(MyService myService){
super();
this.myService = myService;
}
public void run(){
myService.c();
}
}
执行结果:
从执行结果可以看出,持相同类锁的两个静态方法是同步执行的,而持对象锁的方法则与静态方法异步执行,因此,类锁和对象锁并不是同一把锁。
那么这两把锁到底有什么区别呢?
5.1静态Synchronized方法和非静态Synchronized方法的区别
区别:
静态Synchronized方法,多个类对象持一把锁;
非静态Synchronized方法,多个对象多把锁。
同样还是用上边的MyService类作为测试:
A线程:
public class ThreadA extends Thread {
private MyService myService;
public ThreadA(MyService myService){
super();
this.myService = myService;
}
public void run(){
myService.a();
}
public static void main(String[] args) {
// 多个MySevice对象,线程A,B同时执行方法a
MyService myService1 = new MyService();
MyService myService2 = new MyService();
ThreadA threadA = new ThreadA(myService1);
ThreadB threadB = new ThreadB(myService2);
threadA.start();
threadB.start();
}
}
B线程:
public class ThreadB extends Thread {
private MyService myService;
public ThreadB(MyService myService){
super();
this.myService = myService;
}
public void run(){
myService.a();
}
}
执行结果:
静态的Synchronized方法,即使在多个对象的情况下,它也是同步执行的,它并不具备多对象多把锁的特征。
6、Synchronized关键字和内部类及静态内部类
关于内部类:
1.内部类中隐含有外部类的引用。所以可以使用外部类中所有的东西。
2.创建内部类对象时,必须与外围类对象相关联,创建外围类对象之前,不可能创建内部类的对象。
特殊情况:静态内部类(也称嵌套类)
1.创建静态内部类是不需要外部类。
2.静态内部类不能访问外部类的非静态成员。
明白以上内部类的知识后,其实对内部类多线程锁的使用也就基本上明白了,内部类可以看成是放在一个类里边的普通类,因此锁的使用,也就跟普通的类是一样的,关键是要区分锁到底使用的是什么样的锁。
然后静态的内部类只是区别这个内部类跟外部类的关系,在使用锁时,性质与普通内一样。
一般,内部类中有两个同步方法,但使用的却是不同锁,那么执行是异步的。
测试代码:
public class MyService {
static class Child01{
// 使用Child02对象锁
public void method01(Child02 child02){
synchronized (child02) {
try {
System.out.println(Thread.currentThread().getName()+":进入child01的method01方法...");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+":退出child01的method01方法...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 持有child01的对象锁
public synchronized void method02(){
try {
System.out.println(Thread.currentThread().getName()+":进入child01的method02方法...");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+":退出child01的method02方法...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Child02{
// 使用Child02对象锁
public synchronized void method01(){
try {
System.out.println(Thread.currentThread().getName()+":进入child02的method01方法...");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+":退出child02的method01方法...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Child01 child01 = new Child01();
Child02 child02 = new Child02();
// 线程1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 传入child02作为锁对象
child01.method01(child02);
}
});
// 线程2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// 使用child01对象作为锁
child01.method02();
}
});
// 线程3
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
// 使用child02对象作为锁
child02.method01();
}
});
t1.start();
t2.start();
t3.start();
}
}
执行结果:
分析上述执行结果:
线程1和线程0是异步执行的,这是因为两个方法所持有的锁不同,线程1持有的是Child02的锁,线程0持有的是Child01的锁,锁不一样,所以执行异步。
线程2和线程0是同步执行的,它们共同争夺Child02的锁,所以线程2必须等线程1执行完才能继续执行,在线程0执行时,线程2成阻塞状态。
总之,不管是普通内还是内部类,关键看的还是持有的锁对象是否相同,相同则同步执行,不同则异步执行。