作者:叩丁狼教育王一飞,高级讲师。转载请注明出处。
名称解释
共享资源:在多线程并发环境下,多个线程可以操作同一个数据,这数据便是共享资源
临界资源:一次仅允许一个线程使用的资源称为临界资源
竞争资源:多线程并发环境下,为保证线程安全无误执行,对操作数据有序的争夺.这类数据称之为竞争资源.
线程同步:同步就是协同步调,按预定的先后次序运行. 线程同步, 当一个线程在对数据进行操作时,其他线程都不可以对这个数据进行操作,直到该线程完成操作, 其他线程才能对该数据进行操作.
线程安全:在多线程并发环境下, 线程能通过同步机制保证各个线程都可以正常且正确的执行,不出现数据污染等意外情况
临界区:指程序中的一个代码段,在这段代码中,有且只有一个线程可对其进行访问。java中可以synchronized关键字标识一个临界区.
对象锁: java每一个对象都拥有一把锁(对象锁),约定线程能进入临界区前提, 必须持有事先设置好的对象锁.
类锁: Class对象锁。
获取锁: 当前线程为进入临界区而争夺到的锁对象.
synchronized 同步方法使用
先看一个案例:2个线程同时操作同一个成员变量
public class RedPacket implements Runnable{
private int count = 5; //默认5个
public void run() {
//红包数大于0, 可以抢
while (count > 0){
//放大效果
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
grabRed();
}
}
public void grabRed(){
if(count > 0){
System.out.println(Thread.currentThread().getName() + " 抢了编码为:" + count +"红包");
count--;
}
}
}
public class App {
public static void main(String[] args) {
RedPacket red = new RedPacket();
//2个线程抢红包
Thread t1 = new Thread(red, "t1");
Thread t2 = new Thread(red, "t2");
t1.start();
t2.start();
}
}
打印结果:
t1 抢了编码为:5红包
t1 抢了编码为:3红包
t2 抢了编码为:3红包
t1 抢了编码为:1红包
t2 抢了编码为:1红包
上述打印结果,发现几个问题:
1>3号红包跟1号红包被抢了2次,
2>4号2号红包不见了
这是什么原因呢?其实很简单,JVM调度线程,为线程分配CPU的使用权时,是随机的,带有很强的概率性。所以,当多个线程对同一个资源(红包数量count)读写操作时,线程t1 t2执行的顺序也是随机。这就导致前面讲的线程安全问题。
解决:
使用synchronized关键字修饰grabRed方法即可:
public synchronized void grabRed(){
if(count > 0){
System.out.println(Thread.currentThread().getName() + " 抢了编码为:" + count +"红包");
count--;
}
}
synchronized修饰了grabRed() 方法, 当第一个线程执行grabRed方法时, 马上获取当前对象(RedPacket)对象锁,进而拥有执行grabRed方法执行权。在线程没有执行完grabRed方法,释放对象锁前,所有需要调用grabRed方法线程都必须等待。这可以类比,多个人要蹲坑,一人手快,先进坑了,随手锁门。其他人只能在外面等着,等上个人出来在进。
使用注意
1>synchronized 修饰的方法,表示持有当前操作对象的对象锁
2>synchronized 持有对象锁是重入的
锁重入:简单的讲,是同一个线程可以重复获取同一个对象锁
public class Resource implements Runnable{
public void run() {
}
public synchronized void method1(){
System.out.println("调用了方法1.....");
method2(); //调用方法2
}
public synchronized void method2(){
System.out.println("调用了方法.....");
}
}
上面代码, 线程t1可以执行method1方法,也可顺利调用method2方法,这种重复获取Resource对象锁操作,称之为锁重入。这里要注意,线程t1在执行method1方法时,如果线程t2想执行method2时,必须要等待。因为method1, method2执行前提都是相同的Resource对象锁, 而此时对象锁在线程t1手里,所以线程t2必须得等。
3>线程执行方法时,抛出异常,对象锁会自动释放
public class ResouceExt implements Runnable{
public void run() {
doSomething();
}
public synchronized void doSomething(){
System.out.println(Thread.currentThread().getName() + " 开始。。。。");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(1/0); //模拟异常
}
}
public class App {
public static void main(String[] args) throws InterruptedException {
ResouceExt resouceExt = new ResouceExt();
Thread t1 = new Thread(resouceExt, "t1");
Thread t2 = new Thread(resouceExt, "t2");
t1.start();
Thread.sleep(1000);
t2.start();
System.out.println("----");
}
}
这个容易理解, 出现1/0 除0异常,doSomething() 方法不做任何处理,向run方法抛,run方法不做处理,那继续向上抛,此时run方法也结束了,那么对应线程就没有持有对象锁的必要了,就可以释放锁了。
4>synchronized持有的锁无法被子类继承
public class Father implements Runnable {
public void run() {
}
//注意父类方法使用synchronized修饰
public synchronized void doSomething(){
System.out.println("父类dosomething方法.....");
}
}
//继承父类
public class Son extends Father {
public void run() {
doSomething();
}
//重写父类方法:注意此时不用synchronized 修饰
@Override
public void doSomething() {
System.out.println("当前线程:" + Thread.currentThread().getName() + ",进来了....");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程:" + Thread.currentThread().getName() + ",出去了....");
}
}
public class App {
public static void main(String[] args) throws InterruptedException {
Son son = new Son();
Thread t1 = new Thread(son, "t1");
Thread t2 = new Thread(son, "t2");
t1.start();
t2.start();
System.out.println("----");
}
}
结果:t1 t2 可以一起进来,一起出去
----
当前线程:t1,进来了....
当前线程:t2,进来了....
当前线程:t1,出去了....
当前线程:t2,出去了....
Son子类的重写父类的doSomething方法,没有使用synchronized修饰,线程t1, 线程t2无需要获取锁便可以进入。说明了synchronized修饰的方法没有继承性。
5>synchronized修饰的方法是静态方法时,线程持有的锁是当前类的字节码对象锁, 也即类锁.
synchronized同步代码块
同步代码操作语法:
synchronized(对象锁){
//需要同步的代码逻辑块
}
看一个案例:开始4个线程模拟4个售票口卖票
public class Ticket implements Runnable {
private int count = 10;
public void run() {
while (count > 0) {
System.out.println(Thread.currentThread().getName() + "卖出" + count + "号票");
count--;
//放大效果..
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class App {
public static void main(String[] args) {
Ticket ticket = new Ticket();
//4个线程售票
Thread t1 = new Thread(ticket, "t1");
Thread t2 = new Thread(ticket, "t2");
Thread t3 = new Thread(ticket, "t3");
Thread t4 = new Thread(ticket, "t4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
结果: 卖出重号的票 10号 与 5号
t1卖出10号票
t2卖出10号票
t3卖出8号票
t4卖出7号票
t2卖出6号票
t1卖出5号票
t4卖出5号票
t3卖出3号票
t3卖出2号票
t4卖出1号票
t2卖出1号票
分析:
重号的原因是多线程争夺count的时候,出岔子, 比如说, 重号的5号票, t1拿到5号票时,刚输入:"t1卖出5号票", 还没来得及执行count--操作,那么count=5,就在此时t4线程刚好执行到输出"t4卖出5号票".所以就出现重号的现象.
解决:
从结果反推,打印跟执行count-- 应该同时操作, 不应该割裂开始, 也即, 下面2行代码应该是一体的,同一个时间点, 只允许一个线程操作.
System.out.println(Thread.currentThread().getName() + "卖出" + count + "号票");
count--;
所以可以使用synchronized同步代码块解决
public class Ticket implements Runnable {
private int count = 10;
public void run() {
while (count > 0) {
synchronized (this) {
if (count > 0) { //防止为负数的出现
System.out.println(Thread.currentThread().getName() + "卖出" + count + "号票");
count--;
}
}
//放大效果..
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
分析:
synchronized (this) 包裹的代码块, 表示在同一个时间点,只允许一个线程执行,其他线程需要等待.this就是线程持有的当前对象的对象锁.
注意:
1> synchronized (this) 表示持有当前对象的对象锁
2>synchronized (对象锁) 多个线程要想达到互斥的效果, 对象锁必须同一个
synchronized (new Object()) {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "卖出" + count + "号票");
count--;
}
}
上面操作无法达到同一时间点,只允许一个线程操作效果. 因为 synchronized (new Object()) 每个线程获取都是不同对象锁,起不到互斥的效果.
synchronized 同步方法与synchronized 同步代码块选用
2种操作其实都一样: 多个线程竞争同一个对象锁,持锁者操作, 无锁者等等.
如果硬要说区别,synchronized 同步代码块控制同步的粒度更小而已.