根据《Java(71):多线程学习02-->实现Runnable接口方式实现多线程》一文中1.2【数据资源共享】章节,数据资源共享会遇到多线程不安全问题,多个线程同时操作同1个对象,如果控制不好,就会产生问题,叫做线程不安全,可以用synchronized来解决。
同步锁,synchronized相当于给方法,对象上锁或者给类上锁,这样防止其他线程访问共享资源,进而保护多线程的安全。synchronized的原理是它使用了flag标记ACC_SYN-CHRONIZED,执行线程先持有同步锁,然后执行方法,最后在方法完成时才释放锁。
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
修饰代码块:指定加锁对象,对给定对象/类加锁。synchronized(this / object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得当前 class 的锁。
synchronized(this) {
//代码块
}
示例:
class RunnableTest implements Runnable{
private int ticket =10;
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(this) {
if (this.ticket > 0) {
System.out.println("票号:" + ticket-- + ",已被售卖,售卖窗口:" + Thread.currentThread().getName());
}
}
}
}
}
在调用synchronized修饰的代码块前,要加上等待Thread.sleep(10);否则都会让1个线程都占用了。
调用:
public class RunnableDemo {
public static void main(String[] args){
RunnableTest rt=new RunnableTest();
Thread t1 = new Thread(rt,"一号窗口");
Thread t2 = new Thread(rt,"二号窗口");
Thread t3 = new Thread(rt,"三号窗口");
t1.start();
t2.start();
t3.start();
}
}
执行结果:
当编写的方法体比较大时,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了。将synchronized作用于一个给定的实例对象t,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有sell实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待。当然除了t作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁。
修饰实例方法:作用于当前对象实例方法加锁,进入同步代码前要获得当前对象实例方法的锁。
synchronized void method() {
//业务代码
}
示例:
class RunnableTest implements Runnable{
private int ticket =10;
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(10);
sell();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized private void sell() {
if (this.ticket > 0) {
System.out.println("票号:" + ticket-- + ",已被售卖,售卖窗口:" + Thread.currentThread().getName());
}
}
}
在调用synchronized修饰的sell方法前,要加上等待Thread.sleep(10);否则都会让1个线程都占用了。
调用:
public class RunnableDemo {
public static void main(String[] args){
RunnableTest rt=new RunnableTest();
Thread t1 = new Thread(rt,"一号窗口");
Thread t2 = new Thread(rt,"二号窗口");
Thread t3 = new Thread(rt,"三号窗口");
t1.start();
t2.start();
t3.start();
}
}
执行结果:
数据资源共享,10张票3个窗口(线程执行)来完成。
当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的所有 synchronized 方法,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的synchronized实例方法,这样的方式也就保护了多线程的安全,不过其他线程还是可以访问该实例对象的其他非synchronized方法。
其他:
1、 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
2、每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
3、实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制
建议:尽量减小synchronized代码块的大小。
参考:Java多线程(四) 解决多线程安全
https://zhuanlan.zhihu.com/p/412782802
https://www.cnblogs.com/weibanggang/p/9470718.html