多线程编程
一、线程与进程
进程:一个正在执行的程序就是一个进程,是cpu分配资源的基本单位,一个进程可以包含多个线程,进程之间的切换速度较慢。
线程:是一个进程的一部分,cpu调度的基本单位,线程之间的上下文切换速度很快。
二、创建线程的方法
1、实现Runnable接口
示例代码
public class RunnableThread implements Runnable{
public void run() {
for(int i=0;i<20;i++) {
System.out.println("正在执行线程任务:"+i);
}
};
public static void main(String[]args) {
RunnableThread runnable=new RunnableThread();
Thread thread=new Thread(runnable);
thread.start();
for(int i=0;i<1000;i++) {
System.out.println("主线程正在执行:"+i);
}
}
}
步骤
1.创建类实现runnable接口
2.实现run()方法,编写程序体
3.创建线程对象,执行Thread类对象的start()方法启动线程。
2、继承Thread类
1.1 简介
示例代码
public class ExtendsThread {
public static void main(String[]args) {
NewThread thread=new NewThread();
thread.start();
for(int i=0;i<1000;i++) {
System.out.println("主线程:"+i);
}
}
}
class NewThread extends Thread{
@Override
public void run() {
for(int i=0;i<20;i++) {
System.out.println("内部线程:"+i);
}
}
}
步骤:
1.继承Thread类
2.重写run();
3.创建对象,执行start();
1.2 练习
使用common-io.jar包下的FileUtils工具类多线程下载文件
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import org.apache.commons.io.FileUtils;
//练习thread实现多线程同步下载文件
public class TestThread extends Thread{
private String url;
private String fileName;
public TestThread(String url, String fileName) {
super();
this.url = url;
this.fileName = fileName;
}
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
WebDownload.downloader(url, fileName);
System.out.println("下载了文件"+fileName);
}
//主方法测试
public static void main(String[]args) {
TestThread thread1=new TestThread("https://csdnimg.cn/cdn/content-toolbar/csdn-logo_.png?v=20190924.1","1.jpg");
TestThread thread2=new TestThread("https://csdnimg.cn/cdn/content-toolbar/csdn-logo_.png?v=20190924.1","2.jpg");
thread1.start();
thread2.start();
}
}
//编写下载类
class WebDownload{
public static void downloader(String url,String fileName){
try {
FileUtils.copyURLToFile(new URL(url), new File(fileName));
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
1.3 Thread和Runnable的联系(部分源码分析)
public
class Thread implements Runnable {
Thread类实现Runnable接口
/* What will be run. */
private Runnable target;
其中有一个Runnable类型的属性
public void run() {
if (target != null) {
target.run();
}
}
run方法实际上是执行目标类的target的run方法,实际上Thread类是一个Runnable的代理类,采用静态代理的模式。
注意:
建议使用Runnable接口,避免java单继承机制的局限性,方便灵活,方便一个对象被多个线程使用。例如:有一个子类已经继承了一个类,但开发人员不清楚它是否有父类,此时如果继承Thread类可能就会造成错误,可以通过实现Runnable接口的方式来实现多继承。
3、实现callable接口
步骤:
1.实现Callable接口
2.重写call方法,需要返回结果和抛出异常
3.创建目标对象
4.创建线程池执行服务
ExecutorService es=Executors.newFixThreadPool(1);
5.执行目标对象任务
Futureresult =es.submit(callable);
Boolean b=result.get();
6.关闭服务
es.shutdownNow();
示例代码:
import java.io.PrintStream;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableThread implements Callable{
//重写call方法
@Override
public Object call() throws Exception {
// TODO Auto-generated method stub
// System.out.println("call方法已经执行");
return true;
}
public static void main(String[]args) {
CallableThread callable=new CallableThread();
ExecutorService service=Executors.newFixedThreadPool(1);
Future<Boolean>result=service.submit(callable);
try {
System.out.println(result.get());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
service.shutdownNow();
}
}
注意:
Thread方法并没有实现Callable接口,所以Thread类不能对实现Callable接口的类进行代理;实现Callable接口的类对象只能通过ExecutorService类型的对象进行执行。
三、lamda表达式
**介绍:**lamda为希腊字母第十一位 “入“,lamda表达式的引入是为了避免在代码中匿名内部类过多的情况,其实质是函数式编程的思想。
例子:
//语法:
单行情况下
a->System.out.println(a);
多行情况下
a->{System.out.println(a);
int b=1;
System.out.println(b);}
//例子:
new Thread(()->System.out.println("。。。。。。")).start();
在线程中它就起到了非常奇妙的作用,采用表达式的方式代替了匿名内部类,简化了匿名内部类的编写,是代码变得更加简介,去掉了很多没有意义的代码,只保留逻辑代码。
lamda表达式只能作用于函数式接口:如果一个接口是函数式接口,则可以用过lamda表达式来创建这个接口的对象。
函数式接口:接口中只具有一个抽象方法的接口。
四、线程的状态跃迁
1、线程的状态及其的转换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PkFIHabb-1582296146700)(C:\Users\67229\AppData\Roaming\Typora\typora-user-images\image-20200220144301198.png)]
线程状态分为5中:创建状态、就绪状态、运行状态、阻塞状态和终止zhuangtai。
创建状态:只是完成线程的初始化工作,创建完成,如new Thread();
就绪状态:调用线程的启动方法使线程能够被cpu调度执行,如thread.start();
运行状态:此时线程已经被cpu调度,正在运行
阻塞状态:线程休眠(sleep)、线程等待(wait)或者抢占同步资源进入阻塞队列
终止状态:线程执行完毕或线程异常终止,此过程是不可逆的,一旦线程终止,此线程将不能再次运行。
2、线程终止的方法
在对线程进行终止操作时,不建议使用自带的方法(stop,destroy)或者jdk已经废弃的方法,因为这可能会造成意想不到的结果(如:资源不能得到合理的释放)。建议使用标志位的方式使线程自动终止。
public class StopableThread implements Runnable{
private boolean flag=true;
public void run(){
int i=0;
while(flag){
System.out.println("线程正在运行。。。"+i++);
}
}
public void stop(){
flag=false;
}
}
采用标志位的方式使线程自动终止。
3、线程休眠(sleep)
sleep方法介绍:
sleep(int time): 指定线程阻塞的时间(单位为毫秒);
sleep需要抛出异常:InterruptedException;
sleep可以模拟网络延时、倒计时等;
每个对象都有一个锁,调用sleep线程进行休眠时并不会释放锁。
应用:简单的倒计时
import java.text.SimpleDateFormat;
import java.util.Date;
public class ThreadSleep{
public static void main(String []args) {
//获取系统时间
Date currentTime=new Date(System.currentTimeMillis());
while(true) {
try {
System.out.println(new SimpleDateFormat("HH:mm:ss").format(currentTime));
//线程休眠1秒
Thread.sleep(1000);
//更新当前时间
currentTime=new Date(System.currentTimeMillis());
}catch(Exception e ) {
e.printStackTrace();
}
}
}
}
4、线程礼让(yield:文明的线程)
线程礼让:一个线程A对另一个线程B进行礼让,线程A暂停但并不阻塞,并将状态设置为就绪状态,然后线程A和线程B同时等待CPU调度,但礼让并不一定成功,其结果取决于cpu的调度。
public class TestYield {
public static void main(String[]args) {
new Thread(new YieldThread(),"线程1").start();
new Thread(new YieldThread(),"线程2").start();
}
}
//文明的线程
class YieldThread implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName()+"开始运行");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"执行结束");
}
}
5、线程强制执行(join:可以想象为vip插队)
join合并线程,带此线程执行完毕后,再执行其他线程,其他线程阻塞
public class TestJoin {
public static void main(String args[]) {
//lamda表达式创建join线程
Thread vipThread=new Thread(()->{
for(int i=1;i<=10;i++) {
try {
Thread.sleep(200);
System.out.println("vip线程执行:"+i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
for(int i=1;i<=10;i++) {
if(i==5) {
try {
//vip线程插入
vipThread.start();
vipThread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
Thread.sleep(200);
System.out.println("main线程执行:"+i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
6、查看线程的状态
public class TestState{
public static void main(String[]args){
Thread thread=new Thread(){
public void run(){
try{
Thread.sleap(1000);
}catch(Exception e){
e.printStackTrace();
}
}
}
Thread.State state=thread.getState();
System.out.println(state); //NEW状态
thread.start();
state=thread.getState();
System.out.println(state); //RUN
while(state!=Thread.State.TERMINATED){
Thread.sleep(100);
state=thread.getState();
System.out.println(state);
}
}
}
thread.getState();用来查看线程的状态,返回Thread.State类型的对象
7、线程优先级
java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪一个线程来执行;
线程优先级用数字表示,范围从1到10。
常见的线程优先级常量:
Thread.MIN_PRIORITY=1;
Thread.MAX_PRIORITY=10;
Thread.NORM_PRIORITY=5;
使用以下方法来改变和获取优先级
thread.getPriority(); 获取线程的优先级
thread.setPriority(int priority); 设置线程的优先级
注意:
优先级的设置要在线程的启动前;
线程的优先级低,只是代表着线程被优先调度的概率低,并不意味着不会优先调度
8、守护(daemon)线程
线程分为用户线程和守护线程;
虚拟机必须保证用户线程执行完毕,不用等待守护线程执行完
如后台记录操作日志,监控内存,垃圾回收等待
设置守护线程的方法:
Thread thread=new Thread();
thread.setDaemon(true);
五、线程同步
多个线程操作同一个资源的时候就会涉及到线程同步的问题。
并发:同一个对象被多个线程同时操作
如:抢票,两个银行同时取钱
线程同步=队列+锁;
多线程操作时,当多个线程操作同一内存空间的数据的时候,就会造成数据混乱的情况,这时候就需要进行线程同步;java的解决方法是为每一个对象设置一个锁对象,一个线程只有获取到这个锁对象的时候才能对此对象进行操作。
1、synchronized实现线程同步
synchronized实现线程同步的原理:
java中为每一个对象都关联了一个锁对象,当一个线程要进入synchronized方法或者代码块的时候,首先要获取这个对象的锁,才能执行相应的方法或程序块;若这个对象的锁被另一个线程占有,则此线程需要等待另一个线程释放锁并获取到锁后才能执行相应的方法。
synchronized实现线程同步的两种方式:
在方法前加synchronized关键字:
synchronized public void method1(){...};
synchronized public static void method2(){...};
对与普通方法,synchronized锁的是对象;对于静态方法,synchronized锁的是类(实际上锁的是类的class对象)。
注意:
当两个不同的线程执行同一对象的两个不同的同步方法时,会发生阻塞现象(即同一对象的两个不同的同步方法之间也同步,因为两个线程要获取同一个对象的锁)
测试代码:
public class SynchronizedTest {
public static void main(String[]args) {
Action action=new Action();
Thread thread1=new Thread(()->{action.method1();},"线程1");
Thread thread2=new Thread(()->action.method2(),"线程2");
thread1.start();
thread2.start();
}
}
//测试两个线程调用同一个对象的两个不同的同步方法是否会发生阻塞
class Action{
synchronized public void method1() {
try {
System.out.println("执行方法1的线程:"+Thread.currentThread().getName());
Thread.sleep(10000);
System.out.println("方法1执行完毕");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
synchronized public void method2() {
try {
Thread.sleep(1000);
System.out.println("执行方法2的线程:"+Thread.currentThread().getName());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
线程2要等待线程1执行完毕后才会执行。
此时就会发现如果一个对象中的两个同步方法可以同时执行,在方法前添加synchronized关键字就已经不再适用。此时就要提及synchronized的另一种加锁方式。
synchronized代码块:
synchronized(obj){
}
obj被称为同步监视器,实际上就是加锁对象(即共享资源)
同步监视器可以是任何类型的对象,但是建议将共享资源作为同步监视器。
同步方法实际上也是用的这个原理,只不过普通同步方法共享资源为this(即对象本身),静态同步方法的同步监视器为这个类。
2、死锁
多个线程各自占有一些资源,并且相互等待其他线程占有的资源才能运行,导致两个或多个线程相互等待对方释放资源而停止执行的情况。
死锁示例代码:
public static void main(String[]args){
Thread thread1=new Thread(()->{
synchronized(Integer.class){
System.out.println("线程1获取到Integer类对象");
try{
Thread.sleep(1000);
}catch(Exception e){}
synchronized(String.class){
System.out.println("线程1获取到String类对象");
}
//若未发生死锁则会打印以下内容
System.out.println("线程1执行完毕");
}
});
Thread thread2=new Thread(()->{
synchronized(String.class){
System.out.println("线程2获取到String类对象");
try{
Thread.sleep(1000);
}catch(Exception e){}
synchronized(Integer.class){
System.out.println("线程2获取到Integer类对象");
}
//若未发生死锁则会打印以下内容
System.out.println("线程2执行完毕");
}
});
thread1.start();
thread2.start();
}
结果程序发生死锁。
2.1 死锁避免方法
产生死锁的四个必要条件:
-
互斥条件:一个资源只能被一个进程使用。
-
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
-
不剥夺条件:进程获取的资源,在未使用完之前不能强行释放
-
循环等待条件:若干个进程形成一种头尾相接循环等待对方资源的关系。
只要打破其中的一个条件就能避免死锁:
比如打破条件2:一个线程在请求另一个资源的时候,首先将自己保持的资源释放掉。
打破条件3:当一个线程进入阻塞状态时,首先释放自己的所有锁。
打破条件4:按照一定的顺序获取锁
2.2死锁检测的方法
-
在工作空间的cmd下输入jconsole可以对线程进行监控,通过点击线程模块下的死锁检测便可对线程进行死锁检测。
-
使用Jstack进行死锁检测,步骤如下:
(1) 在cmd中切换到工作空间,输入jps查看进程号
(2) 输入jstack -l [pid]查看其中的线程,若出现死锁将会出现如下信息
-
使用jvisualvm进行死锁检测,同jconsole
3、Lock
jdk5.0开始,java提供了更加强大的线程同步机制----通过显式的定义同步对象实现同步,同步锁使用Lock对象来充当;
java.util.concurrent.locks.Lock接口是控制多线程对共享资源进行访问的工具。每次只能有一个线程对Lock对象加锁,线程访问共享资源之前应先获取Lock对象。
ReentrantLock类实现了Lock接口,它具有与synchronized相同的并发性和内存语义,在线程安全控制中,比较常用,可以显式加锁和释放锁。
ReentrantLock lock=new ReentrantLock();
try{
//加锁
lock.lock();
}catch(Exception e){}
finally{
//释放锁
lock.unlock();}
synchronized和Lock对比
- Lock是显式锁需要手动开启和关闭,synchronized是隐式锁出了作用域自动释放。
- Lock不能锁方法。
- 使用Lock锁,jvm将花费较少的时间来调度线程,性能更好。并且具有更加良好的扩展性(因为Lock为接口)。
- 优先使用顺序
Lock>同步代码块>同步方法。
4、生产者消费者模式
生产者和消费者共用同一块内存空间,生产者只需从共享空间中获取资源,消费者只需往共享空间中放入资源;这样使得生产者和消费者只需关注共享空间,而不需要考虑彼此之间的状态,从而实现多线程的协作。
现实生活中的例子:
没有使用生产者和消费者模式就相当于:你去餐厅吃饭,你需要什么首先要点单,后厨收到订单后要进行做饭(生产),做好后你才能够食用(消费),此时线程之间的协作就比较复杂,而且效率低下(你等的时间长,商家卖出的少)。
而有些商家就比较聪明,在前台准备很多乘菜的大盆,后厨把饭菜实现准备好,你买菜的时候只需要看盆里有没有菜;而后厨也只需盯着菜盆,菜盆不满就做菜。这要是不是感觉就节省的很多时间,并且商家也能卖出更多的菜。(虽然不太健康,但消费者节省了时间,商家卖得更多)
生产者消费者模式的实现有很多中方式,本文中只介绍一种实现,基于wait和notify的方法,实际上就是模拟了一个阻塞队列。
在写代码之前首先要介绍一下wait()和notify()/notifyAll()方法:
wait:当执行wait方法的时候,当前线程就会进入等待状态,同时释放掉自己获取到的锁
notify():使一个等待状态的线程进入运行状态(当然并不一定会运行,还要取决于这个线程是否被cpu调度,所要使用的资源是否获取到等因素)。
注意:
执行wait方法的必须是一个线程,
这个线程必须是同步线程(也就是说必须至少拥有一把锁),
生产者消费者模式示例代码:
public class CP {
public static void main(String args[]) {
BufferedArea bufferedArea=new BufferedArea();
Thread customer=new Thread(new Customer(bufferedArea));
Thread producer=new Thread(new Producer(bufferedArea));
producer.start();
customer.start();
System.out.println("over");
}
}
class BufferedArea{
private Goods goodss[];
private int count;
public BufferedArea(){
goodss=new Goods[10];
count=0;
}
public synchronized void push(Goods goods) {
if(count==10) {
try {
this.wait();
}catch(Exception e) {
e.printStackTrace();
}
}
goodss[count++]=goods;
this.notifyAll();
}
public synchronized Goods poll() {
if(count==0) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
count--;
Goods g=goodss[count];
goodss[count]=null;
this.notifyAll();
return g;
}
}
class Goods{
private int num;
public Goods(int num) {
this.setNum(num);
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
class Customer implements Runnable{
private BufferedArea bufferedArea;
public Customer(BufferedArea bufferedArea) {
this.bufferedArea=bufferedArea;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<10;i++) {
System.out.println("customer poll"+bufferedArea.poll().getNum());
}
}
}
class Producer implements Runnable{
private BufferedArea bufferedArea;
public Producer(BufferedArea bufferedArea) {
this.bufferedArea=bufferedArea;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<10;i++) {
System.out.println("producer push"+i);
bufferedArea.push(new Goods(i));
}
}
}