当多个线程并行访问共享的数据时,通常会发生访问冲突,产生脏数据。可以使用synchronized关键字来避免这种情况出现。
Java中任何对象上都有一把锁,synchronized的使用会让一个对象的锁在一段时间内只能被一个线程占用,让其他线程等待,当获得锁的线程释放锁之后,所有的线程才开始竞争这个锁。
synchronized的用法有三种:
第一种:同步代码块(对象锁)
synchronized作用于代码块,指定锁定的对象,在进入代码块之前需要获得指定对象的锁。
下面代码中的this指的是类的当前实例对象
//售票问题
public class Test3 {
public static void main(String[] args) {
T mythread = new T();
Thread t1 = new Thread(mythread, "一");
Thread t2 = new Thread(mythread, "二");
Thread t3 = new Thread(mythread, "三");
Thread t4 = new Thread(mythread, "四");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class T implements Runnable{
static int num = 100;
@Override
public void run() {
while(num>0) {
synchronized (this) {
if(num<=0) {
System.out.println("售票结束");
break;
}
System.out.println(Thread.currentThread().getName()+
"号窗口售卖出第"+num+"张票");
num--;
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
上述代码运行结果为:
运行结果正确,没有出现脏数据。
第二种:同步方法(对象锁)
synchronized作用于实例方法,向当前实例加锁,在执行同步方法之前,需要获得当前实例的锁。当前实例指调用这个方法的对象。
//售票问题
//同步方法实现
public class Test3 {
public static void main(String[] args) {
T mythread = new T();
Thread t1 = new Thread(mythread, "一");
Thread t2 = new Thread(mythread, "二");
Thread t3 = new Thread(mythread, "三");
Thread t4 = new Thread(mythread, "四");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class T implements Runnable{
static int num = 100;
@Override
public void run() {
while(num>0) {
synMethon();
}
}
private synchronized void synMethon() {
if(num<=0) {
System.out.println("售票结束");
return;
}
System.out.println(Thread.currentThread().getName()+"号窗口售卖出第"+num+"张票");
num--;
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果是:
结果是正确的。每一次只有一个线程执行synMethon()方法。
第三种:同步静态方法(类锁)
synchronized作用于静态方法,会向当前类加锁,即所有被synchronized修饰的静态方法获得同一个锁。当一个线程执行其中一个被synchronized修饰的静态方法时,这个线程就获得了所有被synchronized修饰的静态方法的锁,其他线程必须等待该线程释放锁之后,才能参与锁的竞争。但是非静态方法不在这个锁中。
//当i为奇数时,输出奇数,并且退出程序
public class Test2 {
public static void main(String[] args) {
R1 r1 = new R1(); //创建第一个对象
Thread t = new Thread(r1);
t.start(); //死循环执行 r1.add()
while(true) {
int i = R1.get();
if(i%2!=0) {
System.out.println(i);
System.exit(0);
}
}
}
static class R1 implements Runnable{
static int i;
public static synchronized void add() {
i++;
i++;
}
public static synchronized int get() {
// 测试输出i,看锁的占用情况
System.out.println(i);
return i;
}
@Override
public void run() {
while(true) {
add();
}
}
}
}
测试结果是:
结果中连续打印相同数字的原因:main线程多次抢占类锁的锁,所以多次输出同样的值;打印的值跨度大的原因:t 线程多次抢占类锁,多次执行add()方法,所以跨度较大。
第三种用法还有一种写法,类似于第一种用法
//这是第三种用法的另一种写法,作用是一样的
public void A() {
synchronized (类名.class) {
}
}
总结:
- 第一种和第二种用法都是作用于对象,称对象锁,第二种是专门指向类的实例对象,第一种可以指向其他的引用对象,比如数组。
- 第三种用法作用于类,因为静态方法属于类的成员,一个线程获得类锁之后,锁中的静态方法无法被其他线程调用。需要等到该线程释放类锁之后,再去竞争类锁。