第四讲 多线程的应用(2)
一、线程安全问题的另一解决方案
前面我们已经知道,同步代码块的锁是任意对象,同步方法的锁是this对象,静态方法的锁是类的字节码文件对象。但是前面的方法不够明确,我们很难看到代码是在哪锁的,又是在哪解锁的。为了更清晰的表达在哪里加锁,在哪里解锁,JDK5中提供了Lock锁。
代码实现如下:
package cn.itcast_01;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyLock implements Runnable {
private int ticket = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 加锁
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (ticket--) + "张票");
}
// 释放锁
lock.unlock();
}
}
}
package cn.itcast_01;
public class MyLockDemo {
public static void main(String[] args) {
MyLock ml=new MyLock();
Thread t1=new Thread(ml,"窗口一");
Thread t2=new Thread(ml,"窗口二");
Thread t3=new Thread(ml,"窗口三");
t1.start();
t2.start();
t3.start();
}
}
这样写代码有一个这样的问题,一旦加锁和释放锁之间的代码出现问题,程序就会被锁在这里,无法执行下面的代码,所以加锁的部分我们通常做如下改进。这样无论中间的代码出现什么问题,我都会释放锁。
package cn.itcast_01;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyLock implements Runnable {
private int ticket = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
// 加锁
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (ticket--) + "张票");
}
}
finally {
// 释放锁
lock.unlock();
}
}
}
}
二、死锁问题
我们知道同步线程的执行效率较低,因为每个线程执行前都要先判断锁对象,但是这个不足我们是可以接受的,因为他毕竟解决了线程安全的问题。但是另外一个缺陷是我们接受不了的,就是“死锁”问题,通俗的理解就是钥匙卡在锁里了,谁也打不开锁了,谁也进不了家了。那么究竟什么是“死锁”呢?死锁是指两个或者两个以上的线程在执行过程中,因争夺资源产生的一种相互等待的现象。如果出现同步嵌套,就容易出现“死锁”问题。让我们来看一段代码理解“死锁”出现的情形。
package cn.itcast_02;
public class MyLock {
public static final Object objA=new Object();
public static final Object objB=new Object();
}
package cn.itcast_02;
public class DieLock extends Thread {
private boolean flag = false;
public DieLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (MyLock.objA) {
System.out.println("if objA");
synchronized (MyLock.objB) {
System.out.println("if objB");
}
}
} else {
synchronized (MyLock.objB) {
System.out.println("else objB");
synchronized (MyLock.objA) {
System.out.println("else objA");
}
}
}
}
}
package cn.itcast_02;
public class DieLockDemo {
public static void main(String[] args) {
DieLock dl1=new DieLock(true);
DieLock dl2=new DieLock(false);
dl1.start();
dl2.start();
}
}
三、线程间的通信
1.多线程的两种模型
解决上述的死锁问题,就要用到线程间的通信。为了更透彻的理解死锁问题的解决方案,在这里我们先不谈线程通信怎么解决该问题,我们先谈谈什么是线程通信。
前面的售票的例子我们可以用下面的模型来表示,100张票是死的,三个窗口来卖。
但是生活中的很多现象并不是这样的,例如卖煎饼我们可以用下面的模型来表示。卖煎饼的前端是买煎饼的也就是消费者,而卖煎饼的后边是后端也就是生产者。消费者和生产者在交易过程中是会沟通的,如果还有煎饼可卖,那么生产者就等着,并叫卖让消费者来买;反之,如果没有煎饼了,消费者会等着,并告知生产者需要生产煎饼了。
2.设置、获取线程模型的实现
上面的例子就可以说明线程通信问题,即不同种类的线程间(生产者或消费者)针对同一资源(煎饼)的操作。生产者可以称为设置线程,消费者可以称为获取线程。下面用代码实现上述模型。
package cn.itcast_03;
public class Student {
String name;
int age;
}
package cn.itcast_03;
public class SetThread implements Runnable {
@Override
public void run() {
Student s=new Student();
s.name="刘亦菲";
s.age=27;
}
}
package cn.itcast_03;
public class GetThread implements Runnable {
@Override
public void run() {
Student s=new Student();
System.out.println(s.name+"---"+s.age);
}
}
package cn.itcast_03;
public class StudentThread {
public static void main(String[] args) {
SetThread st=new SetThread();
GetThread gt=new GetThread();
Thread t1=new Thread(st);
Thread t2=new Thread(gt);
t1.start();
t2.start();
}
}
通过执行上述代码发现,并没有出现我们预期的结果,有设置有获取。通过分析发现,原因是两个线程中的对象不是同一个对象,这样就不符合线程通信的“针对同一资源的操作”。举个简单的例子,买煎饼的和卖肉夹馍的在买卖过程中会有交流吗?答案是显而易见的。
那么我们就要对上述代码进行改进,改进的思路很简单,在测试类中新建对象,把该对象最为参数传递到线程中,就能保证操作的是同一个资源。但是这时候要注意,线程中必须存在该种构造方法。改进代码如下。
package cn.itcast_04;
public class SetThread implements Runnable {
private Student s;
private int i = 0;
public SetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
if (i % 2 == 0) {
s.name = "刘亦菲";
s.age = 27;
} else {
s.name = "宋承宪";
s.age = 37;
}
i++;
}
}
}
package cn.itcast_04;
public class GetThread implements Runnable {
private Student s;
public GetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
System.out.println(s.name + "---" + s.age);
}
}
}
package cn.itcast_04;
public class StudentThread {
public static void main(String[] args) {
Student s=new Student();
SetThread st=new SetThread(s);
GetThread gt=new GetThread(s);
Thread t1=new Thread(st);
Thread t2=new Thread(gt);
t1.start();
t2.start();
}
}
3.线程安全问题的解决
通过运行上述代码,很容易就发现代码存在安全问题。主要有两个问题:一是一个数据出现多次,二是姓名和年龄不匹配。经过分析可知,出现第一个问题的原因是CPU一点点时间片的执行权,就够执行多次循环。出现第二个问题的原因是线程运行的随机性。
通过上一讲我们可以知道,解决安全问题可以通过同步来实现,也就是给线程加锁。解决安全问题的代码实现如下。
package cn.itcast_05;
public class SetThread implements Runnable {
private Student s;
private int i = 0;
public SetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if (i % 2 == 0) {
s.name = "刘亦菲";
s.age = 27;
} else {
s.name = "宋承宪";
s.age = 37;
}
}
i++;
}
}
}
package cn.itcast_05;
public class GetThread implements Runnable {
private Student s;
public GetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
System.out.println(s.name + "---" + s.age);
}
}
}
}
4.线程通信问题的实现
通过上述改进,线程的安全问题得到了解决。但是一个数据还是会出现多次,这显然不是我们想要的。如果两个线程之间能建立起联系就好了,假设设置线程先抢到CPU的执行权,他首先检查是否有数据,如果有数据设置线程就等待,并告诉获取线程可以获取值了,如果没有数据他就完成赋值操作,并告诉获取线程可以可以获取值了,以此类推。这就是所谓的等待唤醒机制,下面我们就用等待唤醒机制来修改之前的代码。
这里存在这样一个问题,wait()方法和notify()方法为什么会是Object类中的方法呢?通过查API我们发现notify()方法的描述是这样的,“唤醒在此对象监视器上等待的单个线程”,这里的此对象监视器也就是锁对象,所以wait()方法和notify()都是要通过锁对象来调用的,而锁对象是任意对象,所以决定上述两个方法都是Object类的方法。
package cn.itcast_07;
public class Student {
String name;
int age;
boolean flag;
}
package cn.itcast_07;
public class SetThread implements Runnable {
private Student s;
private int i = 0;
public SetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if (s.flag) {
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (i % 2 == 0) {
s.name = "刘亦菲";
s.age = 27;
} else {
s.name = "宋承宪";
s.age = 37;
}
i++;
// 修改标记
s.flag = true;
// 唤醒线程
s.notify();
}
}
}
}
package cn.itcast_07;
public class GetThread implements Runnable {
private Student s;
public GetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if(!s.flag){
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(s.name + "---" + s.age);
//修改标记
s.flag=false;
//唤醒线程
s.notify();
}
}
}
}
5.代码优化
下面的代码优化主要是优化了以下两项:一、将学生类的成员变量私有化;二、将设置和获取封装到了方法中,并在方法中实现同步。
package cn.itcast_08;
public class Student {
private String name;
private int age;
private boolean flag;
public synchronized void set(String name, int age) {
if (this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
this.age = age;
// 修改标记
this.flag = true;
// 唤醒线程
this.notify();
}
public synchronized void get() {
if (!this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.name + "----" + this.age);
// 修改标记
this.flag = false;
// 唤醒线程
this.notify();
}
}
package cn.itcast_08;
public class SetThread implements Runnable {
private Student s;
private int i = 0;
public SetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
if (i % 2 == 0) {
s.set("刘亦菲", 27);
} else {
s.set("宋承宪", 30);
}
i++;
}
}
}
package cn.itcast_08;
public class GetThread implements Runnable {
private Student s;
public GetThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
s.get();
}
}
}
6.线程状态转化
四、线程组
线程组可以实现对线程的批量设置,比如将线程添加到线程组、获取线程组名称、设置线程组为后台线程、设置线程组最大优先级等,部分功能实现如下
package cn.itcast_09;
public class Group implements Runnable {
@Override
public void run() {
for(int i=1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
package cn.itcast_09;
public class GroupDemo {
public static void main(String[] args) {
//method1();
method2();
}
private static void method2() {
Group g = new Group();
ThreadGroup tg = new ThreadGroup("线程组A");
Thread t1 = new Thread(tg,g,"刘亦菲");
Thread t2 = new Thread(tg,g,"宋承宪");
t1.start();
t2.start();
System.out.println(t1.getThreadGroup().getName());
System.out.println(t2.getThreadGroup().getName());
}
private static void method1() {
Group g = new Group();
Thread t1 = new Thread(g);
Thread t2 = new Thread(g);
/*
* t1.start(); t2.start();
*/
ThreadGroup tg1 = t1.getThreadGroup();
ThreadGroup tg2 = t2.getThreadGroup();
System.out.println(tg1.getName());
System.out.println(tg2.getName());
System.out.println(Thread.currentThread().getThreadGroup().getName());
}
}
五、线程池
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。
线程池中的每一个线程代码结束后,并不会死亡,而是再次回到了线程池成为空闲状态,等待下一个对象来使用。从JDK5开始Java内置支持线程池,提供了Executors类来产生线程池,有如下几个方法:
- public static ExecutorService newCachedThreaPool()
- public static ExecutorService newFixedThreaPool(int nThreads)
- public static ExecutorService newSingleThreaExecutor()
这些方法的返回值是ExecutorService类的对象,该对象就表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程,它提供了如下方法:
- Future
1、实现Runnable接口代码实现如下
package cn.itcast_10;
public class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
package cn.itcast_10;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.shutdown();
}
}
2、实现Callable接口代码实现如下
package cn.itcast_11;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
private int number;
public MyCallable(int number){
this.number=number;
}
@Override
public Integer call() throws Exception {
int sum=0;
for (int i = 1; i <= number; i++) {
sum+=i;
}
return sum;
}
}
package cn.itcast_11;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> f1 = pool.submit(new MyCallable(100));
Future<Integer> f2 = pool.submit(new MyCallable(200));
Integer num1=f1.get();
Integer num2=f2.get();
System.out.println(num1);
System.out.println(num2);
pool.shutdown();
}
}
六、匿名内部类开线程
在我们实际开发中有时仅仅是想开一个线程,不管用之前的继承Thread类还是实现Runnable接口都显得有些麻烦,这时便可以使用匿名内部类来开启线程。代码实现如下
package cn.itcast_12;
public class ThreadDemo {
public static void main(String[] args) {
new Thread() {
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + ":"
+ i);
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("重写接口" + ":"
+ i);
}
}
}) {
}.start();
/*new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("重写Runnable" + ":" + i);
}
}
}) {
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("内部类的方法" + ":" + i);
}
}
}.start();*/
}
}