黑马程序员---第四讲 多线程的应用(2)
来源:互联网 发布:软件数据线6.0 编辑:程序博客网 时间:2024/05/21 10:36
第四讲 多线程的应用(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();*/ }}
- 黑马程序员---第四讲 多线程的应用(2)
- 黑马程序员-DOM(第四讲)
- 黑马程序员-Oracle(第四讲)
- 黑马程序员--多线程的应用(1)
- <黑马程序员> 第四篇:多线程
- 黑马程序员--多线程应用
- 黑马程序员 多线程的理解和应用
- 黑马程序员——多线程2:应用
- 黑马程序员的第四天
- 黑马程序员________多线程的理论及应用学习笔记
- 黑马程序员-关于java多线程的总结及应用
- 黑马程序员-多线程2
- 黑马程序员--多线程2
- 黑马程序员----多线程2
- 黑马程序员--多线程2
- 黑马程序员-多线程2
- 黑马程序员 多线程 2
- 黑马程序员 :(反射应用 )通过反射讲<Integer>类型的集合中添加一个<String>类型的数据
- XML解析二 使用DOM解析XML
- iOS 第三方库汇总(超全!!!!)
- 浅谈RAID写惩罚(Write Penalty)与IOPS计算
- Android fragment spinner 修改默认显示的文本内容
- Unsupported major.minor version 52.0
- 黑马程序员---第四讲 多线程的应用(2)
- 关于Only the original thread that created a view hierarchy can touch its views的解决方案
- Cornerstone Svn简单使用指南
- apache的url_rewrite地址重写中得到问号后面的参数
- Git使用教程
- [.NET源码] C#制作的一套在线更新软件系统
- 获取WebView加载HTML时网页中的内容
- 封装遮盖&下拉菜单
- JabRef 文献管理软件简明教程