如果要说java的第一大特色:多线程的编程
在解释多线程之前首先看一下进程的概念:在操作系统中一个程序的执行周期就称为进程,在一个程序执行的时候一定要使用到电脑的IO CPU 内存等,在最初DOS系统的时代由于其本身就是一个单进程的操作系统,所以在同一个时间段上只能有一个程序执行。回来到了windows时代,多个程序可以同时执行,所以windows是多进程系统
但是这个时候问题就出现了,不管有多少个CPU实际上处理的逻辑是没有太大变化的,依然只有一块空间进行程序处理,假设单CPU的情况下,多个进程要同时进行,就需要进行资源的轮番使用
一块资源在同一个时间段上,可能有多个进程交替执行,但是在一个时间点上只会有一个进程执行。
而线程指的是在进程基础上的进一步划分,也就是说线程是比进程更小的单位,很明显线程的启动所花费的时间一定是短的,也就是说进程要比线程慢,那么Java本身是一个多线程的编程语言,所以其执行的性能一定很快。
思考:java中的多线程引用体现在哪里呢?
所谓的高并发指的就是访问的线程量暴高,高并发最直白的问题是:服务器的内存不够用了,无法处理新的用户。
线程是在进程基础上的划分,没有进程就没有线程,进程一旦消失了,线程一定会消失。
Thread类实现多线程
如果要想实现多线程,那么必须有一个前提:有一个线程的执行主类,
如果现在要想实现一个多线程的主类,有两类途径:
继承一个Thread类、
【推荐】实现Runnable、Callable接口。
java.lang.Thread是一个线程操作的核心类,如果现在想定义一个线程的主类,那么最简单的方法就是直接继承Thread的类,覆写该类中的run()方法(相当于线程的主方法)。
范例:定义线程主体类
1 package cn.Tony.demo; 2 3 class MyThread extends Thread{//是一个线程的主体类 4 private String title; 5 public MyThread(String title) { 6 this.title=title; 7 } 8 @Override 9 public void run() { //所有的线程从此出开始执行 10 for(int x=0;x<10;x++) { 11 System.out.println(this.title+",x="+x); 12 } 13 } 14 } 15 public class TestDemo { 16 public static void main(String[] args) { 17 18 } 19 } 20
当现在有了线程的主体类之后,很明显首先就一定会想到产生对象随后调用方法,但是你现在不能够直接调用run()方法。
范例:观察直接调用run()方法
1 public class TestDemo { 2 public static void main(String[] args) { 3 MyThread mt1=new MyThread("线程A"); 4 MyThread mt2=new MyThread("线程B"); 5 MyThread mt3=new MyThread("线程C"); 6 mt1.run(); 7 mt2.run(); 8 mt3.run(); 9 } 10 }
这个时候只是做了一个顺序打印,而跟多线程没关洗。多线程的执行实际上和进程是很相似的,也应该采用多个程序交替执行。正确启动多线程的方式应该调用Thread类中的start()方法
启动多线程:public void start();调用此方法会调用run();
范例:正确的启动多线程
1 public class TestDemo { 2 public static void main(String[] args) { 3 MyThread mt1=new MyThread("线程A"); 4 MyThread mt2=new MyThread("线程B"); 5 MyThread mt3=new MyThread("线程C"); 6 mt1.start(); 7 mt2.start(); 8 mt3.start(); 9 } 10 }
此时再次执行代码发现所有的线程对象变为了交替执行
疑问:为什么要通过start()方法来调用run()方法?而不是run()方法直接执行
如果要想解决此类问题,就必须打开Java的一个源代码进行浏览(在JDK安装目录下);
1 public synchronized void start() { 2 if (threadStatus != 0) 3 throw new IllegalThreadStateException(); 4 group.add(this); 5 6 boolean started = false; 7 try { 8 start0(); 9 started = true; 10 } finally { 11 try { 12 if (!started) { 13 group.threadStartFailed(this); 14 } 15 } catch (Throwable ignore) { 16 } 17
首先在本程序中该方法会抛出一个异常“IllegalThreadStateException()”,如果按照原本的处理方式,这个时候在start()方法上应该使用throws声明,或者在方法中进行try..carch处理,而此处不需要处理。所以它是一个RuntimeException的子类。这个异常的产生只是在你重复启动了多线程才会抛出。
所以每个线程对象只能启动一次,
而后发现在start()方法里面调用了一次start0()方法,而这个方法是一个只声明而未实现方法,同时使用native关键子定义,native指的是调用本机的原升系统函数。
使用Runnable接口实现多线程
Thread类的核心功能是线程的启动,但是如果一个类直接继承了Thread类,但是如果一个类直接继承了Thread类所造成的问题就是单继承局限,那么在java中有提供另外一种实现模式:Runnable接口,那么首先来观察一下此接口的定义:
@FunctionInterface
public interface Runnable{
public void run();
}
可以发现接口里面同时存在一个run()方法,这一点和Thread相同,因为只要是接口的子类就必须覆写run()方法。
范例:利用Runnable定义主体类
1 class MyThread implements Runnable{ 2 private String title; 3 public MyThread(String title) { 4 this.title=title; 5 } 6 @Override 7 public void run() { 8 for(int x=0;x<10;x++) { 9 System.out.println(title+",x="+x); 10 } 11 } 12 }
但是新的问题出现了,此时MyThread类不再是Thread的类,而实现了Runnable接口。虽然解决了单继承,但是没有了start()方法被继承了。那么此时要关注Thread类提供的构造方法。
构造方法:public Thread(Runnable target),可以接受我们Runnable接口对象,那么就意味着通过Thread可以启动
范例:启用多线程
1 public class TestDemo { 2 public static void main(String[] args) { 3 MyThread m1=new MyThread("线程A"); 4 MyThread m2=new MyThread("线程B"); 5 MyThread m3=new MyThread("线程C"); 6 new Thread(m1).start(); 7 new Thread(m2).start(); 8 new Thread(m3).start(); 9 } 10 }
这个时候启动了多线程,也就是说多线程的启动永远都是Thread类的start()方法。
但是需要注意,此时Runnable接口对象也可以采用匿名内部类或者Lambda表达式来定义
范例:通过匿名内部类
1 package cn.Tony.demo; 2 3 public class TestDemo { 4 public static void main(String[] args) { 5 new Thread(new Runnable() { 6 public void run() { 7 System.out.println("Hello World!"); 8 } 9 }).start(); 10 } 11 }
范例:使用Lambda处理
1 package cn.Tony.demo; 2 3 public class TestDemo { 4 public static void main(String[] args) { 5 new Thread(() ->System.out.println("Hello World!")).start(); 6 } 7 } 8
Thread和Runnableq区别
首先从使用形式上来讲明显要使用Runnable实现多线程会更好,实际上Thread和Runnable直接也存在一些联系,Thread继承定义形式。
public class Thread extends Object implements Runnable
Thread的是Runnable接口的子类,那么Thread类也应该覆写run()方法,
线程的继承结构
所有在多线程的处理上使用的是代理设计模式。除了以上的关系之外,实际上,在开发中,使用Runnable还有一个特点,使用Runnable 实现的多线程的程序类可以更好的描述出数据共享的概念(并不是说Thread不能)。
希望产生若干个线程同一数据进行操作。
范例:使用Thread实现数据操作
1 package cn.Tony.demo; 2 class MyThread extends Thread{ 3 private int ticket=10; 4 @Override 5 public void run() { 6 for(int x=0;x<10;x++) { 7 if(this.ticket>0) { 8 System.out.println("卖票,ticket="+this.ticket--); 9 } 10 } 11 } 12 } 13 public class TestDemo { 14 public static void main(String[] args) { 15 extracted(); 16 } 17 private static void extracted() { 18 new MyThread().start(); 19 new MyThread().start(); 20 new MyThread().start(); 21 } 22 } 23
此时只是想启动三个线程进行卖票处理,结果变为了各自卖各自的三张票。
范例:利用Runnable来实现数据操作
1 package cn.Tony.demo; 2 class MyThread implements Runnable{ 3 private int ticket=10; 4 @Override 5 public void run() { 6 for(int x=0;x<10;x++) { 7 if(this.ticket>0) { 8 System.out.println("卖票,ticket="+this.ticket--); 9 } 10 } 11 } 12 } 13 public class TestDemo { 14 public static void main(String[] args) { 15 MyThread mt=new MyThread(); 16 new Thread(mt).tart(); 17 new Thread(mt).start(); 18 new Thread(mt).start(); 19 } 20 } 21
Runnable比Thread有更好的数据共享操作。
线程的运行状态
线程的启动使用的是start()方法,但是并不意味着你调用start()方法就立刻启动多线程,
当我们多线程调用了start()方法的之后并不是立刻执行,而是进入到就绪状态,等待进行调度后执行,需要将资源分配运行后,才可以执行多线程中的代码(run()方法中的代码)。
当你执行了一段时间之后,你需要让出资源,让其他线程执行,可能run()方法还没有执行完,只执行到一半,那么看要让出资源,随后重新进入到就绪状态,重新等待分配资源,当线程执行完毕后才会进入到终止状态。
通过Callable来实现多线程
从JDK1.5开始追加了一个新的开发包:java.util.concurrent,这个开发包主要是进行高新能编程使用的,也就是说在这个开发包里面会提供一些高并发操作中才会使用到的类,在这个包里面定义有一个新接口
1 @FunctionalInterface 2 public interface Callable<V>{ 3 public V call() throws Exception; 4 }
Runnable中的run()方法虽然也是线程的主方法,但是其没有返回值,因为它的设计遵循了主方法的原则,线程开始了别回头,但是很多时候需要一些返回值,例如:当某些线程执行完成后有可能带来一些返回结果,这种情况下只能Callable来实现多线程。
范例:使用Callable定义线程
1 class MyThread implements Callable<String>{ 2 @Override 3 public String call() throws Exception { 4 for(int x=0;x<20;x++) { 5 System.out.println("卖票,x="+x); 6 } 7 return "票卖完了,下次吧"; 8 } 9 }
不管什么情况,要想启动多线程,只有Thread类的start()方法。分析Callable接口的定义,public FutureTask(Callable<V> callable)
范例:启动并取得多线程的结果
1 public class TestDemo { 2 public static void main(String[] args) throws Exception { 3 FutureTask<String> task=new FutureTask<String>(new MyThread()); 4 new Thread(task).start(); 5 System.out.println(task.get()); 6 } 7 }
这种形式主要返回处理结果。
多线程常用操作方法
线程命名和取得
多线程主要的操作方法都在Thread类里面,也就是说所谓的方法就是查询文档的过程,开发很少会出现多线程的代码,所以重点掌握核心方法,
多线程的运行状态是不确定的,所以对于多线程的操作必须有一个明确标识出线程对象的信息,那么往往都使用名称来描述,那么在Thread类里面提供有如下的名称操作方法,
1.创建线程对象设置名字:public Thread(Runnable targer,String name)
2.设置线程名字:public final void setName(String name)
3.确定线程名字:public final void getName()
但是如果要想取得线程对象,在Thread类面提供有一个方法
取得当前线程对象:public static Thread currentThread()
范例:观察线程名称的取得
1 package cn.Tony.demo; 2 3 class MyThread implements Runnable{ 4 @Override 5 public void run() { 6 for(int x=0;x<10;x++) { 7 System.out.println(Thread.currentThread()+",x="+x); 8 } 9 } 10 } 11 public class TestDemo { 12 public static void main(String[] args) throws Exception { 13 MyThread mt=new MyThread(); 14 new Thread(mt).start();//无名称 15 new Thread(mt).start();//无名称 16 new Thread(mt,"有线程名称").start();//有名称 17 } 18 } 19
范例:观察线程的执行结果
1 package cn.Tony.demo; 2 3 class MyThread implements Runnable{ 4 @Override 5 public void run() { 6 System.out.println(Thread.currentThread().getName()); 7 } 8 } 9 public class TestDemo { 10 public static void main(String[] args) throws Exception { 11 MyThread mt=new MyThread(); 12 mt.run(); 13 new Thread(mt).start(); 14 } 15 } 16
实际上通过以上程序可以发现主方法本身就是一个线程,所有的线程都是通过主线程创建并启动的,
疑问:进程在哪里?
实际上没当使用了java命令去解释的时候,都表示启动了一个新的JVM进程,而主方法只是进程上一个线程,1台电脑可以启动多个进程!
线程休眠
所谓的线程休眠指的是让线程暂缓执行一下,等到了预计时间之后再恢复执行,
方法:public static void sleep(Long millis) throws InterruptedException
休眠的时间用毫秒作为单位。
范例:处理休眠操作
1 class MyThread implements Runnable{ 2 @Override 3 public void run() { 4 for(int x=0;x<1000;x++) { 5 try { 6 Thread.sleep(3000); 7 } catch (InterruptedException e) { 8 // TODO Auto-generated catch block 9 e.printStackTrace(); 10 } 11 System.out.println(Thread.currentThread().getName()+",x="+x); 12 } 13 } 14 }
通过代码错误的观察到。线程是3个同时进行的,所有代码依次进入到run()方法中。
正在进去到方法的对象可以是一个,也有可能是多个,所以所有的进入代码的顺序可能有差异,但是总体执行是并发执行,
线程的优先级
所谓的线程优先级优先级越高越有可能先执行,在Thread类里面提供以下的优先级操作方法,
设置优先级:public final void setPriority(int newPriority)
确定优先级:public final int getPriority()
对于优先级设置的内容可以通过Thread类的几个常量来决定。
最高优先级:public static final int MAX_PRIORITY 10
中等优先级:public static final int NORM_PRIORITY 5
最低优先级:public static final int MIN_PRIORITY 1
范例:设置优先级
主方法的优先级为中等优先级
线程的同步与死锁是整个的多线程里面最需要重点理解的概念,这种操作的核心在于:每一个线程对象轮番抢占资源问题。
同步问题的引出
现在希望做一个简单的程序:是希望让其可以实现多个卖票的出理。所以初期的实现如下,
1 class MyThread implements Runnable{ 2 private int ticket=10;//这里是一共卖出票的总数 3 @Override 4 public void run() { 5 for(int x=0;x<20;x++) { 6 try { 7 Thread.sleep(200); 8 } catch (InterruptedException e) { 9 e.printStackTrace(); 10 } 11 if(this.ticket>0) {//表示还有票 12 System.out.println(Thread.currentThread().getName()+"卖票,ticket="+this.ticket--); 13 } 14 } 15 } 16 }
发现票数为负数了,所以这种操作就称为不同步操作,
不同步唯一好处是处理速度快(多个线程并发执行),去银行,是一个业务员对应一个客户,
同步处理
如果要想实现这把“锁”的功能,那么也可以采用synchronized 有两种处理模式:
1.同步代码块
如果想要同步代码块必须要设置一个要锁定的对象,一帮可以锁定当前对象this
1 class MyThread implements Runnable{ 2 private int ticket=10;//这里是一共卖出票的总数 3 @Override 4 public void run() { 5 for(int x=0;x<20;x++) { 6 synchronized(this) {//在同一时刻为线程上锁,表示为程序逻辑上锁 7 if(this.ticket>0) {//表示还有票 8 try { 9 Thread.sleep(200); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 System.out.println(Thread.currentThread().getName()+"卖票,ticket="+this.ticket--); 14 } 15 } 16 } 17 } 18 }
第一种方式是在方法里拦截的,也就是说进入方法中的线程可能依然会有很多个。
2.同步方法
1 class MyThread implements Runnable{ 2 private int ticket=10;//这里是一共卖出票的总数 3 @Override 4 public void run() { 5 for(int x=0;x<20;x++) { 6 sale(); 7 } 8 } 9 public synchronized void sale() { 10 if(this.ticket>0) {//表示还有票 11 try { 12 Thread.sleep(200); 13 } catch (InterruptedException e) { 14 e.printStackTrace(); 15 } 16 System.out.println(Thread.currentThread().getName()+"卖票,ticket="+this.ticket--); 17 } 18 } 19 }
同步可以保证数据的完整性,线程安全操作。但是其执行的速度会很慢!
死锁
同步实质在于:一个线程等待另一个线程执行完毕后才可以继续执行。但是如果现在相关
的几个线程彼此枝江都在等待着(同步),那么就会造成死锁。
范例:模拟一个死锁
1 package cn.Tony.demo; 2 3 class Pen{ 4 public synchronized void get(Note note) { 5 System.out.println("为了得到账本"); 6 note.result(); 7 } 8 public synchronized void result() { 9 System.out.println("为了涂鸦"); 10 } 11 } 12 class Note{ 13 public synchronized void get(Pen pen) { 14 System.out.println("为了那笔"); 15 pen.result(); 16 } 17 public synchronized void result() { 18 System.out.println("为了上厕所"); 19 } 20 } 21 public class DeadLock implements Runnable{ 22 private static Note note=new Note(); 23 private static Pen pen=new Pen(); 24 public static void main(String[] args) { 25 new DeadLock(); 26 } 27 public DeadLock() { 28 new Thread(this).start(); 29 pen.get(note); 30 } 31 public void run() { 32 note.get(pen); 33 } 34 35 }
死锁一旦出现之后实际上整个程序就将暂时性中断执行了,所以死锁属于严重性问题,几率不高,那么整体概念,数据想完成同步就要使用synchronized,如果太多的同步可能就会造成死锁,那些用同步的时候要进行考虑