java中的线程同步锁可以是任意对象。
三种用法:
1.作用于实例方法。当前实例枷锁,进入同步代码前要获取当前实例的锁。
2.作用于静态方法。当前类加锁,进入同步代码前要获取当前类对象的锁。
3.作用于代码块。需要指定加锁的对象,对所给的对象加锁,进入同步代码前要获取当前对象的锁。
一.作用于实例方法
public class ThreadTest extends Thread{
int count = 0;
@Override
public synchronized void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"----"+count++);
}
}
public static void main(String[] args) {
ThreadTest threadTest1 = new ThreadTest();
ThreadTest threadTest2 = new ThreadTest();
Thread thread1 = new Thread(threadTest1,"thread1");
Thread thread2 = new Thread(threadTest1,"thread2");
thread1.start();
thread2.start();
}
}
thread1----0
thread1----1
thread1----2
thread1----3
thread1----4
thread2----5
thread2----6
thread2----7
thread2----8
thread2----9
代码中开启了两个线程去操作一个共享变量count,count++是先读取值再写值。如果第一个线程执行这个过程,第二个线程拿到之前的count值,那么就造成线程不安全了。所以在run方法上面加上synchronized,获取对象锁,代码中的实例对象就是threadTest1。
当一个线程正在访问一个对象synchronized实例方法时,别的线程时访问不了的,当然也访问不 了该对象的synchronized方法,但是可以访问其他未加锁的方法。
线程thread1访问的实例对象threadTest1的synchronized方法(当前对象锁是threadTest1),线程thread2访问的实例对象threadTest2的synchronized方法(当前对象锁是threadTest1),这样是允许的,因为两个实例对象不同,此时如果线程访问非共享数据,线程是安全的,如果两个线程访问的是共享数据,那么线程就不安全了。
public class ThreadTest extends Thread{
int count = 0;
@SneakyThrows
@Override
public synchronized void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"----"+count++);
}
}
public static void main(String[] args) {
ThreadTest threadTest1 = new ThreadTest();
ThreadTest threadTest2 = new ThreadTest();
Thread thread1 = new Thread(threadTest1,"thread1");
Thread thread2 = new Thread(threadTest2,"thread2");
thread1.start();
thread2.start();
}
}
thread1----0
thread1----1
thread1----2
thread1----3
thread1----4
thread2----0
thread2----1
thread2----2
thread2----3
thread2----4
两个线程同时去拿共享变量count。但是new了两个实例,这就意味着存在两个实例锁,thread1和thread2分别进入了threadTest1和threadTest2的实例锁,当然线程就不安全了。
二. 作用于静态方法
静态方法不属于当前实例,而是属性类的,那么这个锁就是类的class对象锁。
public class ThreadTest extends Thread{
static int count = 0;
@Override
public synchronized void run() {
increaseCount();
}
private synchronized static void increaseCount(){
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"----"+count++);
}
}
public static void main(String[] args) {
ThreadTest threadTest1 = new ThreadTest();
ThreadTest threadTest2 = new ThreadTest();
Thread thread1 = new Thread(threadTest1,"thread1");
Thread thread2 = new Thread(threadTest2,"thread2");
thread1.start();
thread2.start();
}
}
thread1----0
thread1----1
thread1----2
thread1----3
thread1----4
thread2----5
thread2----6
thread2----7
thread2----8
thread2----9
同样new了两个不同实例,缺线程安全了。静态方法不属于当前实例,而是属于类,所以这个方法里其实就用了同一把锁,这个锁就是类的class对象锁。
三.作用于代码块
使用场景:在某些情况下,我们编写的方法体可能很大,同时存在一些耗时的操作,而需要同步的代码只有一小部分,如果直接对整个方法进行同步操作,性能会降低。此时我们可以使用同步代码块对需要同步的代码包裹,这样就不需要对整个方法进行同步操作了。所以它的操作范围为synchronized(object){}的这个大括号里。
public class ThreadTest extends Thread{
static int count = 0;
@Override
public void run() {
increaseCount();
}
private void increaseCount(){
synchronized (this){
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"----"+count++);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ThreadTest threadTest1 = new ThreadTest();
ThreadTest threadTest2 = new ThreadTest();
Thread thread1 = new Thread(threadTest1,"thread1");
Thread thread2 = new Thread(threadTest2,"thread2");
thread1.start();
thread2.start();
}
}
thread1----0
thread2----0
thread2----1
thread1----1
thread1----2
thread2----3
thread2----4
thread1----5
thread2----6
thread1----7
从输出结果来看,线程不安全,因为我们指定锁为this,指向的是调用这个方法的实例对象。我们new了两个不同的对象threadTest1和threadTest2,所以有两把锁。,分别进入自己传入的对象锁的线程执行increaseCount方法,所以线程不安全。如果想保证线程安全,则在synchronized后面括号写入ThreadTest.class,表示这个类作为锁,这样尽管不同的实例对象调用该方法,线程也是安全的。
总结:
1.修饰普通方法(实例方法),一个对象中的加锁方法只能允许一个线程访问,但是要注意这种情况下锁的是该方法的实例对象,如果多个线程不同对象访问该方法,则线程不安全。
2.修饰静态方法,由于静态方法是类方法,所以这种情况下锁的是包含这个方法的类,也就是类class对象,如果多个线程不同对象访问该静态方法,也可以保证线程安全。
3.修饰代码块,其中普通代码块参数可以是类的一个属性也可以是当前对象,它的同步效果和修饰普通方法一样,如果代码块参数是当前class类对象,则于修饰静态方法类似。