黑马程序员技术交流社区
标题: 【南京校区】Java多线程与并发库高级应用 [打印本页]
作者: 大蓝鲸小蟀锅 时间: 2018-11-16 23:03
标题: 【南京校区】Java多线程与并发库高级应用
Java多线程与并发库高级应用
01. 传统线程技术回顾
传统是相对于JDK1.5而言的
传统线程技术与JDK1.5的线程并发库
线程就是程序的一条执行线索/线路。
创建线程的两种传统方式
1. 创建Thread的子类,覆盖其中的run方法,运行这个子类的start方法即可开启线程
Thread thread = new Thread()
{ @Override
public void run()
{
while (true)
{
获取当前线程对象 获取线程名字
Thread.currentThread() threadObj.getName()
让线程暂停,休眠,此方法会抛出中断异常InterruptedException
Thread.sleep(毫秒值);
}
}
};
thread.start();
2. 创建Thread时传递一个实现Runnable接口的对象实例
Thread thread = new Thread(new Runnable()
{
public void run()
{}
});
thread.start();
问题:下边的线程运行的是Thread子类中的方法还是实现Runnable接口类的方法
new Thread(
b、传递实现Runnable接口的对象
new Runnable()
{
public void run()
{}
}
){
a、覆盖Thread子类run方法
public void run(){}
}.start();
分析:new Thread(Runnable.run()){run()}.start();
子类run方法实际就是覆盖父类中的run方法,如果覆盖了就用子类的run方法,不会再找Runnable中的run方法了,所以运行的是子类中的run方法
总结:
由Thread类中的run方法源代码中看出,两种传统创建线程的方式都是在调用Thread对象的run方法,如果Thread对象的run方法没有被覆盖,并且像上边的问题那样为Thread对象传递了一个Runnable对象,就会调用Runnable对象的run方法。
多线程并不一定会提高程序的运行效率。举例:一个人同时在三张桌子做馒头
多线程下载:并不是自己电脑快了,而是抢到更多服务器资源。例:服务器为一个客户分配一个20K的线程下载,你用多个线程,服务器以为是多个用户就分配了多个20K的资源给你。
02. 传统定时器技术回顾
传统定时器的创建:直接使用定时器类Timer
a、过多长时间后炸
new Timer().schedule(TimerTask定时任务, Date time定的时间);
b、过多长时间后炸,以后每隔多少时间再炸
new Timer().schedule(TimerTask定时任务, Long延迟(第一次执行)时间, Long间隔时间);
TimerTask与Runnable类似,有一个run方法
Timer是定时器对象,到时间后会触发炸弹(TimerTask)对象
示例:
new Timer().schedule(
new TimerTask()定时执行的任务
{
public void run()
{
SOP(“bombing”);
}
显示计时信息
while (true)
{
SOP(new Date().getSeconds());
Thread.sleep(1000);
}
},
10 定好的延迟时间,10秒以后执行任务
);
问题:2秒后炸,爆炸后每隔3秒再炸一次
定时器2秒后炸,炸弹里还有定时器(每3秒炸一次)
class MyTimerTask extends TimerTask 这就是准备用的子母弹
{
public void run()
{
本身就是一颗炸弹
SOP(bombing);
内部子弹
new Timer().schedule(
new MyTimerTask(), 2000
);
}
}
放置子母弹,2秒后引爆
new Timer().schedule(new MyTimerTask(), 2000);
问题延伸:
上面的问题延伸,母弹炸过后,子弹每隔3秒炸一次,再每隔8秒炸一次
1、在MyTimerTask内部定义一个静态变量记录炸弹号,在run方法内将炸弹号加1,每次产生新炸弹,号码就会加1,根据炸弹号判断是3秒炸还是8秒炸。
注意:内部类中不能声明静态变量
定义一个静态变量private static count = 0;
在run方法内部:count=(count+1)%2;
将定时器的时间设置为:2000+2000*count
2、用两个炸弹来完成,A炸弹炸完后启动定时器安装B炸弹,B炸弹炸完后也启动一个定时器安装A炸弹。
定时器还可以设置具体时间,如某年某月某日某时……可以设置周一到周五做某事,自己设置的话需要换算日期时间,可以使用开源工具quartz来完成。
03. 传统线程互斥技术
线程安全问题例子:银行转账
同一个账户一边进行出账操作(自己交学费),另一边进行入账操作(别人给自己付款),线程不同步带来的安全问题
示例:逐个字符的方式打印字符串
class Outputer
{
public void output(String name)
{
int len = name.length();
for (int i=0; i<len; i++)
SOP(name.charAt(i));逐个字符打印
SOP();换行
}
}
public void test()
{
Outputer outputer = new Outputer();
new Thread(
new Runnable()
{
public void run()
{
Thread.sleep(100);
outputer.output(“zhangxiaoxiang”);
}
}).start();
new Thread(
new Runnable()
{
public void run()
{
Thread.sleep(100);
outputer.output(“lihuoming”);
}
}).start();
}
要实现互斥,在这个位置必须使用同一个对象
使用name就达不到同步效果
使用output对象即可达到同步效果
要避免下边产生的问题,左边方法体中的代码要实现原子性
有一个线程正在使用这个方法的代码,别的线程就不能再使用。
就和厕所里的坑一样,已经有人在用了,别人就不能再去用了。
Java中某段代码要实现排他性,就将这段代码用synchronized关键字保护起来。
同步锁可以用任意对象,相当于门锁
synchronized(name)
{
for (int i=0; i<len; i++)
SOP(name.charAt(i));逐个字符打印
SOP();换行
}
这样的话,有一个线程进入保护区域后,没出来的话,别的线程就不能进入保护区域。
注意:
内部类不能访问局部变量,要访问需加final
静态方法中不能创建内部类的实例对象
打印结果发现的问题:线程不同步所致,两个线程都在使用同一个对象
互斥方法:
a、同步代码块
synchronized (lock){}
b、同步方法
方法返回值前加synchronized
同步方法上边用的锁就是this对象
静态同步方法使用的锁是该方法所在的class文件对象
使用synchronized关键字实现互斥,要保证同步的地方使用的是同一个锁对象
public synchronized void output(String name)
{
int len = name.length();
这里就不要再加同步了,加上极易出现死锁
for (int i=0; i<len; i++)
SOP(name.charAt(i));逐个字符打印
SOP();换行
}
04. 传统线程同步通信技术
面试题,子线程10次与主线程100次来回循环执行50次
下面是我刚看完面试题就暂停视频自己试着写的代码,还可以,结果完成要求了
在单次循环结束后让这个刚结束循环的线程休眠,保证另一个线程可以抢到执行权。
public class ThreadInterViewTest
{
/**
* 刚看到面试题没看答案之前试写
* 子线程循环10次,回主线程循环100次,
* 再到子线程循环10次,再回主线程循环100次
* 如此循环50次
*/
public static void main(String[] args)
{
int num = 0;
while (num++<50)
{
new Thread(new Runnable()
{
@Override
public void run()
{
circle("子线程运行", 10);
}
}).start();
try
{
//加这句是保证上边的子线程先运行,刚开始没加,主线程就先开了
Thread.sleep(2000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
circle("主线程", 100);
}
}
public static synchronized void circle(String name, int count)
{
for (int i=1; i<=count; i++)
{
System.out.println(name+"::"+i);
}
try
{
Thread.sleep(5000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
1、将子线程和主线程中要同步的方法进行封装,加上同步关键字实现同步
2、两个线程间隔运行,添加一个标记变量进行比较以实现相互通信,加色的部分
wait notify notifyAll wait会抛出异常
class Business
{
private boolean bShouleSub = true;
public synchronized void sub()
{
if (bShouleSub)
{
for (int i=1; i<11; i++)
SOP(sub+i);
bShouldSub = false;
this.notify();
}
else
this.wait();
}
public synchronized void main()
{
if (!bShouldSub)
//此处使用while以增加程序健壮性,因为存在虚假唤醒,有时候并没有被notify就醒了。如果该方法没有同步的话,此处就更要使用while进行判断了,避免进程不同步问题
{
for (int i=1; i<101; i++)
SOP(main+i);
bShouldSub = true;
this.notify();
}
else
this.wait();
}
}
经验:要用到共同数据(包括同步锁)或相同算法的多个方法要封装在一个类中
锁是上在代表要操作的资源类的内部方法中的,而不是上在线程代码中的。这样写出来的类就是天然同步的,只要使用的是同一个new出来的对象,那么这个对象就具有同步互斥特性
判断唤醒等待标记时使用while增加程序健壮性,防止伪唤醒
05. 线程范围内共享变量的概念与作用
线程范围内共享数据图解:
代码演示:
class ThreadScopeShareData
{
三个模块共享数据,主线程模块和AB模块
private static int data = 0; 准备共享的数据
存放各个线程对应的数据
private Map<Thread, Integer> threadData = new HashMap<Thread, Integer>();
public static void main(String[] args)
{ 创建两个线程
for (int i=0; i<2; i++)
{
new Thread(
new Runnable()
{
public void run()
{现在当前线程中修改一下数据,给出修改信息
data = new Random().nextInt();
SOP(Thread.currentThread().getName()+将数据改为+data);
将线程信息和对应数据存储起来
threadData.put(Thread.currentThread(), data);
使用两个不同的模块操作这个数据,看结果
new A().get();
new B().get();
}
}
).start();
}
}
static class A
{
public void get()
{
data = threadData.get(Thread.currentThread());
SOP(A+Thread.currentThread().getName()+拿到的数据+data);
}
}
static class B
{
public void get()
{
data = threadData.get(Thread.currentThread());
SOP(B+Thread.currentThread().getName()+拿到的数据+data);
}
}
}
结果并没与实现线程间的数据同步,两个线程使用的是同一个线程的数据。要解决这个问题,可以将每个线程用到的数据与对应的线程号存放到一个map集合中,使用数据时从这个集合中根据线程号获取对应线程的数据。代码实现:上面红色部分
程序中存在的问题:获取的数据与设置的数据不同步
Thread-1共享数据设置为:-679705777777
Thread-1--A 模块数据:-679705777777
Thread-0共享数据设置为:11858818
Thread-0--A 模块数据:11858818
Thread-0--B 模块数据:-679705777777
Thread-1--B 模块数据:-679705777777
最好将Runnable中设置数据的方法也写在对应的模块中,与获取数据模块互斥,以保证数据同步
06. ThreadLocal类及应用技巧
多个模块在同一个线程中运行时要共享同一份数据,实现线程范围内的数据共享可以用上一节中所用的方法。
JDK1.5提供了ThreadLocal类来方便实现线程范围内的数据共享,它的作用就相当于上一节中的Map。
每个线程调用全局ThreadLocal对象的set方法,就相当于往其内部的map集合中增加一条记录,key就是各自的线程,value就是各自的set方法传进去的值。
在线程结束时可以调用ThreadLocal.clear()方法用来更快释放内存,也可以不调用,因为线程结束后也可以自动释放相关的ThreadLocal变量。
一个ThreadLocal对象只能记录一个线程内部的一个共享变量,需要记录多个共享数据,可以创建多个ThreadLocal对象,或者将这些数据进行封装,将封装后的数据对象存入ThreadLocal对象中。
将数据对象封装成单例,同时提供线程范围内的共享数据的设置和获取方法,提供已经封装好了的线程范围内的对象实例,使用时只需获取实例对象即可实现数据的线程范围内的共享,因为该对象已经是当前线程范围内的对象了。下边给出张老师的优雅代码:
package cn.itheima;
import java.util.Random;
public class ThreadLocalShareDataDemo
{ /**06. ThreadLocal类及应用技巧
* 将线程范围内共享数据进行封装,封装到一个单独的数据类中,提供设置获取方法
* 将该类单例化,提供获取实例对象的方法,获取到的实例对象是已经封装好的当前线程范围内的对象
*/
public static void main(String[] args)
{
for (int i=0; i<2; i++)
{
new Thread(
new Runnable()
{
public void run()
{
int data = new Random().nextInt(889);
System.out.println(Thread.currentThread().getName()+"产生数据:"+data);
MyData myData = MyData.getInstance();
myData.setAge(data);
myData.setName("Name:"+data);
new A().get();
new B().get();
}
}).start();
}
}
static class A
{ //可以直接使用获取到的线程范围内的对象实例调用相应方法
String name = MyData.getInstance().getName();
int age = MyData.getInstance().getAge();
public void get()
{
System.out.println(Thread.currentThread().getName()+"-- AA name:"+name+"...age:"+age);
}
}
static class B
{
//可以直接使用获取到的线程范围内的对象实例调用相应方法
String name = MyData.getInstance().getName();
int age = MyData.getInstance().getAge();
public void get()
{
System.out.println(Thread.currentThread().getName()+"-- BB name:"+name+"...age:"+age);
}
}
static class MyData
{
private String name;
private int age;
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public int getAge()
{
return age;
}
public void setAge(int age)
{
this.age = age;
}
//单例
private MyData() {};
//提供获取实例方法
public static MyData getInstance()
{
//从当前线程范围内数据集中获取实例对象
MyData instance = threadLocal.get();
if (instance==null)
{
instance = new MyData();
threadLocal.set(instance);
}
return instance;
}
//将实例对象存入当前线程范围内数据集中
static ThreadLocal<MyData> threadLocal = new ThreadLocal<MyData>();
}
}
07. 多个线程之间共享数据的方式探讨
例子:卖票:多个窗口同时卖这100张票,票就需要多个线程共享
a、如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个对象中有共享数据。
卖票就可以这样做,每个窗口都在做卖票任务,卖的票都是同一个数据。
b、如果每个线程执行的代码不同,就需要使用不同的Runnable对象,有两种方式实现
Runnable对象之间的数据共享:
a)将共享数据单独封装到一个对象中,同时在对象中提供操作这些共享数据的方法,可以方便实现对共享数据各项操作的互斥和通信。
b)将各个Runnable对象作为某个类的内部类,共享数据作为外部类的成员变量,对共享数据的操作方法也在外部类中提供,以便实现互斥和通信,内部类的Runnable对象调用外部类中操作共享数据的方法即可。
注意:要同步互斥的几段代码最好分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
08Java5线程并发库的应用
如果没有线程池,需要在run方法中不停判断,还有没有任务需要执行
线程池的通俗比喻:接待客户,为每个客户都安排一个工作人员,接待完成后该工作人员就废掉。服务器每收到一个客户请求就为其分配一个线程提供服务,服务结束后销毁线程,不断创建、销毁线程,影响性能。
线程池:先创建多个线程放在线程池中,当有任务需要执行时,从线程池中找一个空闲线程执行任务,任务完成后,并不销毁线程,而是返回线程池,等待新的任务安排。
线程池编程中,任务是提交给整个线程池的,并不是提交给某个具体的线程,而是由线程池从中挑选一个空闲线程来运行任务。一个线程同时只能执行一个任务,可以同时向一个线程池提交多个任务。
线程池创建方法:
a、创建一个拥有固定线程数的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
b、创建一个缓存线程池 线程池中的线程数根据任务多少自动增删 动态变化
ExecutorService threadPool = Executors.newCacheThreadPool();
c、创建一个只有一个线程的线程池 与单线程一样 但好处是保证池子里有一个线程,
当线程意外死亡,会自动产生一个替补线程,始终有一个线程存活
ExecutorService threadPool = Executors.newSingleThreadExector();
往线程池中添加任务
threadPool.executor(Runnable)
关闭线程池:
threadPool.shutdown() 线程全部空闲,没有任务就关闭线程池
threadPool.shutdownNow() 不管任务有没有做完,都关掉
用线程池启动定时器:
a、创建调度线程池,提交任务 延迟指定时间后执行任务
Executors.newScheduledThreadPool(线程数).schedule(Runnable, 延迟时间,时间单位);
b、创建调度线程池,提交任务, 延迟指定时间执行任务后,间隔指定时间循环执行
Executors.newScheduledThreadPool(线程数).schedule(Runnable, 延迟时间,
间隔时间,时间单位);
所有的 schedule
方法都接受相对 延迟和周期作为参数,而不是绝对的时间或日期。将以 Date 所表示的绝对时间转换成要求的形式很容易。例如,要安排在某个以后的 Date
运行,可以使用:schedule(task, date.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS)
。 Callable与Future的应用:获取一个线程的运行结果
public interface Callable<V>
返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做 call
的方法。 Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable
不会返回结果,并且无法抛出经过检查的异常。 只有一个方法
V call()
计算结果,如果无法计算结果,则抛出一个Exception
异常。使用方法:
ExecutorService threadPool = Executors.newSingleThreadExccutor();
如果不需要返回结果,就用executor方法 调用submit方法返回一个Future对象
Future<T> future = threadPool.submit(new Callable<T>(){//接收一个Callable接口的实例对象
覆盖Callable接口中的call方法,抛出异常
public T call() throws Exception
{
ruturn T
}
});
获取Future接收的结果
future。get();会抛出异常
future.get()没有拿到结果就会一直等待
Future取得的结果类型和Callable返回的结果类型必须一致,通过泛型实现。Callable要通过ExecutorService的submit方法提交,返回的Future对象可以取消任务。
public interface Future<V>
Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由 cancel 方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。
|
| cancel(boolean mayInterruptIfRunning) 试图取消对此任务的执行。 |
| get() 如有必要,等待计算完成,然后获取其结果。 |
| get(long timeout, TimeUnit unit) 如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。 |
| |
| |
public interface CompletionService<V>
CompletionService用于提交一组Callable任务,其take方法返回一个已完成的Callable任务对应的Future对象。好比同时种几块麦子等待收割,收割时哪块先熟先收哪块。
将生产新的异步任务与使用已完成任务的结果分离开来的服务。生产者 submit 执行的任务。使用者 take 已完成的任务,并按照完成这些任务的顺序处理它们的结果。例如,CompletionService 可以用来管理异步 IO ,执行读操作的任务作为程序或系统的一部分提交,然后,当完成读操作时,会在程序的不同部分执行其他操作,执行操作的顺序可能与所请求的顺序不同。
|
| poll() 获取并移除表示下一个已完成任务的 Future,如果不存在这样的任务,则返回 null。 |
| poll(long timeout, TimeUnit unit) 获取并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则将等待指定的时间(如果有必要)。 |
| |
| submit( Runnable task, V result) 提交要执行的 Runnable 任务,并返回一个表示任务完成的 Future,可以提取或轮询此任务。 |
| take() 获取并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则等待。 |
|
| |
| |
示例:
ExecutorService threadPool = Executors.newFixedThreadPool(10); //创建线程池,传递给coms
用threadPool执行任务,执行的任务返回结果都是整数
CompletionService<Integer> coms = new ExecutorCompletionService<Integer>(threadPool);
提交10个任务 种麦子
for (int i=0; i<10; i++)
{
final int num = i+1;
coms.submit(new Callable<Integer>(){
public Integer call() 覆盖call方法
{匿名内部类使用外部变量要用final修饰
SOP(任务+num);
Thread.sleep(new Random().nextInt(6)*1000);
return num;
}
});
}
等待收获 割麦子
for (int i=0; i<10; i++)
{ take获取第一个Future对象,用get获取结果
SOP(coms.take().get());
}
java5的线程锁技术
java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类,
|
| |
| Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。 |
| ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。 |
|
| |
| |
| 为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。 |
| |
| 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。 |
| |
| |
| |
Lock比传统线程模型中的synchronized更加面向对象,锁本身也是一个对象,两个线程执行的代码要实现同步互斥效果,就要使用同一个锁对象。锁要上在要操作的资源类的内部方法中,而不是线程代码中。
public interface Lock
所有已知实现类:
随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized 方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句:
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finally 或 try-catch 加以保护,以确保在必要时释放锁。
Lock与synchronized对比,打印字符串例子
欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) |
黑马程序员IT技术论坛 X3.2 |