前一篇讲到线程的两种实现方式,一种继承Thread类,一种实现了Runabble接口;也说到多个线程共享同一资源问题,由于线程在多cpu环境下是并行执行的,就会出现线程安全问题,此篇幅,我就来讲解一下多线程访问统一资源的安全问题:
(一)通过同步代码块和同步方法,在多线程之间通过加同步锁(synchronized)来操作如何访问共同资源的线程安全问题!
(1)同步代码块:
语法:synchronized(同步锁){
//需要同步操作的代码
}
同步锁:也称为同步监听器,同步监听对象,互斥锁(锁定的是多个线程共享的资源,切记不是当前线程对象)
目的:为了保证每个线程都能正常进行原子性操作(操作为一个整体,不可分割),Java引入了线程同步的机制;Java程序运行使用的任何对象都可作为同步监听对象,但是通常我们会把当前并发访问的共同资源作为同步监听对象(只有共同访问的临界资源,多线程之间才有互斥现象)。
注意:在任何时候访问共同资源(临界资源),最多只允许一个线程拥有同步锁,谁先获得锁就先进入同步代码块,其他的线程就只能等待!
//目的就是解决num--这两个操作时同步的,并且是原子性!(两者可分割)使用同步代码块解决!
public class SynchronizedBlockDemo {
public static void main(String[] args) {
// 创建申请那个线程(同学),吃苹果
Apple1 a = new Apple1(); // Apple对象只有一个(资源共享)
// 三个线程共享Apple中的成员变量资源(num资源)
new Thread(a, "小A").start();
new Thread(a, "小B").start();
new Thread(a, "小C").start();
}
}
class Apple1 implements Runnable {
private int num = 50;
public void run() {
//synchronized (this) { //加锁机制正确,锁定当前Apple1临界资源对象
for (int i = 0; i < 50; i++) {
//synchronized (this) { //加锁机制正确,做出了判断操作!
if (num > 0) {
synchronized (this) { //错误加锁,线程A、B、C同时拿到苹果编号为1时,进行减一操作,不判断,就会出现0、 -1 的情况!
// 模拟网络延迟
try {
// A、B、C 线程同时拿到编号为1的苹果,C线程吃了该苹果,减一,打印;线程A也吃了该苹果,减一操作,打印;
// 线程B也吃了该苹果,也减一操作,打印,就得到了这种结果!
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
String threadName = Thread.currentThread().getName();// 获取当前的线程,进而获取线程名称
System.out.println(threadName + "吃了编号为:" + num + "的苹果!");
num--;
}
}
//}
}
//}
}
}
(2)使用synchronized关键字修饰的方法,叫做同步方法,保证了A线程在执行该方法外,其他线程只能在方法外面等待;
语法:public synchronized void doWork(){
//编写同步代码
}
那么同步锁是谁呢?
1)对于非static修饰的方法,同步锁就是this,当前类的实例对象(谁调用该方法,谁就锁定了当前类的对象,即临界资源对象-->Apple 对象,类实例对象);
2)对于static方法,相当于锁定当前类;
注意:不能使用synchronized关键字修饰run()方法,修饰之后,某一个线程就执行完所有的功能,达不到多个线程共同访问临界资源问题,好比是多个线程串行!(多线程多行道,这样操作就是多个线程串行在单行道中,不会同时在多行道中访问!)
解决方案:把需要同步操作的代码定义在一个新的方法中,并且该方法使用synchronized修饰,再在run()方法中调用该新的方法即可!
锁定同步方法,相当于锁定当前类的实例对象!(多线程访问的临界资源对象Apple对象)
建议:由于使用synchronized加锁机制来实现多线程同步,并且安全,但是效率极低,所以要尽量减少synchronized的作用域(越小越好)
//目的就是解决num--这两个操作时同步的,并且是原子性!(两者可分割)使用同步方法解决!
public class SynchronizedMethodDemo {
public static void main(String[] args) {
// 创建申请那个线程(同学),吃苹果
Apple2 a = new Apple2(); // Apple对象只有一个(资源共享)
// 三个线程共享Apple中的成员变量资源(num资源)
new Thread(a, "小A").start();
new Thread(a, "小B").start();
new Thread(a, "小C").start();
}
}
class Apple2 implements Runnable {
private int num = 50;
public void run() {
for (int i = 0; i < 50; i++) {
eatApple();
}
}
private synchronized void eatApple(){
if (num > 0) {
// 模拟网络延迟
String threadName = Thread.currentThread().getName();// 获取当前的线程,进而获取线程名称
System.out.println(threadName + "吃了编号为:" + num + "的苹果!");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
}
}
}
(3)同步锁(Lock)
Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock全部都有,除此之外,Lock锁机制更加灵活,更加能体现面向对象思想;Lock的实现类为ReentrantLock类,ReentrantLock将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有,调用 lock 的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。
public class LockDemo {
public static void main(String[] args) {
// 创建申请那个线程(同学),吃苹果
Apple3 a = new Apple3(); // Apple对象只有一个(资源共享)
// 三个线程共享Apple中的成员变量资源(num资源)
new Thread(a, "小A").start();
new Thread(a, "小B").start();
new Thread(a, "小C").start();
}
}
class Apple3 implements Runnable {
private int num = 50;
//创建一个可重入锁
private final Lock lock = new ReentrantLock();
private void eatApple(){
//进入方法,立马加锁
lock.lock();//获得锁
if(num > 0){
try{
String threadName = Thread.currentThread().getName();// 获取当前的线程,进而获取线程名称
System.out.println(threadName + "吃了编号为:" + num + "的苹果!");
Thread.sleep(10);
num--;
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
//释放锁
lock.unlock();
}
}
}
public void run() {
for (int i = 0; i < 50; i++) {
eatApple();
}
}
}
(二)synchronized 加锁修饰类的静态变量、方法以及this、非静态方法的讲解:
(1)synchronized修饰非静态方法,锁定的是该类的实例,共享同一个实例在多线程中调用才会触发同步锁定,所以多个被synchronized修饰的非静态方法在同一个实例下,多线程只能同时调用一个,因为互斥。
public class TestThread {
public static void main(String[] args) {
Animal animal = new Animal();
Thread t = new Thread(animal);
t.start();
animal.run1();
}
}
class Animal implements Runnable{
public synchronized void run1(){
System.out.println(System.currentTimeMillis() + " run1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是run1");
System.out.println(System.currentTimeMillis() + " run1");
}
@Override
public void run() {
synchronized(this){
System.out.println(System.currentTimeMillis() + " run");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是run1");
System.out.println(System.currentTimeMillis() + " run");
}
}
}
运行结果:
1497519926575 run1
我是run1
1497519926676 run1
1497519926676 run
我是run1
1497519928676 run
解释:锁定类中静态方法和this,都指的是当前类的实例对象,多个线程在访问这个类中非静态方法,以及同步代码块,由于锁定的同一个实例对象,谁先获得同步锁(即锁定当前临界资源对象),谁就先执行,执行完释放锁时,下一线程方可才能获取同步锁,然后执行操作!
(2)synchronized修饰的静态方法或者静态变量,锁定的是类本身,而不是类的一个实例对象,共享同一个类中所有被synchronized修饰的静态方法或者静态变量,由于互斥,多线程中只能同时调用一个;
public class TestStaticThread {
public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();
Thread t = new Thread(dog);
t.start();
Thread.sleep(100);
Dog.run1();
}
}
class Dog implements Runnable{
private static volatile int num = 0;
public synchronized static void run1(){
System.out.println(System.currentTimeMillis() + "---run1----" + num);
try {
Thread.sleep(100);
num = 100;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是run1");
System.out.println(System.currentTimeMillis() + " ----run1--- " + num);
}
@Override
public void run() {
System.out.println("=====================");
synchronized(Dog.class){
System.out.println(System.currentTimeMillis() + "----run----" + num);
try {
Thread.sleep(2000);
num = 200;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是run");
System.out.println(System.currentTimeMillis() + "----run-----" + num);
}
}
}
运行结果:
=====================
1497519997173----run----0
我是run
1497519999173----run-----200
1497519999173---run1----200
我是run1
1497519999273 ----run1--- 100
解释:synchronized修饰的静态方法,同步锁定的是当前类本身,而不是当前类的实例对象;静态变量和静态方法是属于某一个类,而不是类的实例对象,但类实例可以访问;通过同步代码块锁定当前类,以及同时访问类中的静态方法,多线程之间当然也会有一个先来后到的顺序,由于锁定的同一个类,谁先获的同步锁,谁就先执行,后续线程通过抢占式,谁先获取到同步锁谁就执行!
(3)synchronized块,直接加锁指定的对象,该对象在多个地方被同步锁锁定,多线程也只能同时执行其中的一个,其他未获得锁的线程需要等待期释放锁,然后获取所方可执行!