一、 互斥锁的概念
我们知道,一个进程中的多个线程是可以共享这个进程的系统资源的。如果多个线程同时修改统一个资源(对象)就会导致这个资源的不稳定性和某一时刻的不准确性。
于是,为了保证共享数据操作的完整性,在Java语言中,引入了对象互斥锁的概念。每个对象都对应于一个可称为“互斥锁”的标记,这个标记保证在任一时刻,只能有一个线程访问该对象。
关键字synchronized来与对象的互斥锁来联系,当某个对象被synchronized修饰时,表明在任一时刻只能有一个线程访问该对象。
二、 synchronized的使用
这里只介绍它的简单应用
2.1 锁定某个对象
2.2 锁定某个方法
示例程序如下:
public class TestThreadSync implements Runnable {
int i = 10;
// 锁定method1方法
public synchronized void method1() {
i = 1000;
try {
Thread.sleep(5000);
System.out.println("i= " + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void method2() {
System.out.println(i);
}
public void run() {
method1();
}
public static void main(String[] args) throws Exception {
// 1、启动线程thread会调用method1方法,并锁定
TestThreadSync ts = new TestThreadSync();
Thread thread = new Thread(ts);
thread.start();
// 2、虽然锁定了method1方法,但我们依然可以访问没有的锁定的method2方法,并读出i现有的值
Thread.sleep(1000);
ts.method2();// 输出为1000,而不是10
}
}
三、 死锁
我们都学过著名的“哲学家就餐”的死锁问题,这里我们用一个简单示例来模拟一个死锁。示例程序如下:
定义线程类:
/**
* 线程死锁问题
*
* zhipeng
*
*/
public class TestDeadLock implements Runnable {
public int flag = 1;
// 这里注意,一定要为静态
public static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
// 如果flag==1那么线程会先锁定对象01,然后休眠5毫秒,然后它想继续锁定对象o2,然后才能执行完并释放对象o1的锁
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println(1);
}
}
}
// 如果flag==0那么线程会先锁定对象02,然后休眠5毫秒,然后它想继续锁定对象o1,然后才能执行完并释放对象02的锁
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println(0);
}
}
}
}
}
测试方法:
/**
* zhipeng
*
* 同时启动两个线程,形成死锁
*/
public static void main(String[] args) {
TestDeadLock td1 = new TestDeadLock();
TestDeadLock td2 = new TestDeadLock();
td1.flag = 1;
td2.flag = 0;
Thread thread1 = new Thread(td1);
Thread thread2 = new Thread(td2);
// 两个线程并发执行,则thread1线程会先锁定对象o1,thread2线程会先锁定对象o2。然后thread1等待锁定对象o2,thread2等待锁定对象o1,这样两者就会形成一个死锁,导致程序无法执行
thread1.start();
thread2.start();
}
运行结果:
四、 生产者消费者问题
生产者消费者问题,是经典的线程同步问题。这里其实主要涉及到多个线程间的相互通信,主要涉及到wait()、notify()—唤醒、notifyAll()—唤醒多个线程,这几个方法。
4.1 wait与sleep的区别
sleep为Thread类的静态方法,线程sleep时不会释放掉资源的锁,sleep一段时间后线程会自动醒来。
wait是Object类的方法,线程wait是会释放掉资源的锁,但线程一旦wait 不会自动醒来,需要另一个线程来唤醒(notify/notifyAll)它。
4.2 模拟生产者消费者(共有5个类)
1、定义馒头类:
/**
* 窝头类
*
* @author wangzhipeng
*
*/
public class WoTo {
private int id;
WoTo(int id) {
this.id = id;
}
public String toString() {
return "WOTO: " + id;
}
}
2、定义馒头仓库类:
/**
* 用数组模拟窝头的【仓库】类
*
* @author wangzhipeng
*
*/
public class SyncStack {
int index = 0;
WoTo[] arrywWoTo = new WoTo[6];
/**
* 生产者生产馒头
*/
public synchronized void push(WoTo woTo) {
while (index == arrywWoTo.length) {
try {
this.wait();// 关键:1、如果仓库放满了馒头,则当前(生产者)线程wait睡眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify();// 关键:2、然后唤醒消费者线程进行消费(如果有多个消费者则用notifyAll())
arrywWoTo[index] = woTo;
index++;
}
/**
* 消费者消费馒头
*/
public synchronized WoTo pop() {
while (index == 0) {
try {
this.wait();// 关键:1、如果仓库没有了 馒头,则当前(消费者)线程wait睡眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify();// 关键:2、然后唤醒生产者线程进行生产(如果有多个生产者则用notifyAll())
index--;
return arrywWoTo[index];
}
}
这个类是整个环节的核心,里面包含生产馒头、消费馒头两个方法。注意看里面的代码wait()与notify()。
3、定义生产者(线程)类:
/**
* 生产者线程类
*
* @author wangzhipeng
*
*/
public class Producer implements Runnable {
SyncStack ss = null;
Producer(SyncStack ss) {
this.ss = ss;
}
// 生产馒头,向馒头工厂SyncStack中存放馒头
public void run() {
for (int i = 0; i < 20; i++) {
WoTo woTo = new WoTo(i);
ss.push(woTo);
System.out.println("生产了馒头:" + i);
try {
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4、定义消费者(线程)类:
/**
* 消费者线程类
*
* @author wangzhipeng
*
*/
public class Consumer implements Runnable {
SyncStack ss = null;
Consumer(SyncStack ss) {
this.ss = ss;
}
// 消费馒头,从馒头工厂SyncStack中取出馒头
public void run() {
for (int i = 0; i < 20; i++) {
WoTo woTo = ss.pop();
System.out.println("消费了馒头:" + woTo);
try {
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
5、测试程序类:
public class ProducerConsumer {
/**
* 生产者-消费者,测试类
*
*/
public static void main(String[] args) {
SyncStack ss = new SyncStack();// 初始化馒头工厂类
Producer p = new Producer(ss);// 初始化1个生产者(可以多个)
Consumer c = new Consumer(ss);// 初始化1个消费者
new Thread(p).start();// 启动生产者线程类,开始生产馒头
new Thread(c).start();// 启动消费者线程类,开始消费馒头
}
}
五、 总结
多线程操作时我们需要遵循一个原则:允许多个线程同时读,不允许多个线程同时写。如果需要多线程操作,那么我们就需要考虑清楚如何加锁,要避免死锁问题;还要屡清楚多个线程间如何通信,即线程同步问题等。