多线程是Java语言的重要特性,线程是程序的实体。总之进程可以理解为一个可以独立运行的程序单位,进程是由一个或多个线程组成的,每一个线程就是进程中的一条执行路径。(可以理解为多个线程同时并发执行)
进程:执行中的程序叫做进程(进程是动态的概念,程序是静态的概念)。
线程:一个进程可以产生多个线程,同多个进程可以共享操作系统的某些资源一样,同一进程的多个线程也可以共享此进程的某些资源(比如:代码、数据),所以线程又被称为轻量级进程(lightweight process)。
进程和线程的区别:
1、进程是一段正在执行的程序,是资源分配的基本单元,而线程是CPU调度的基本单元。
2、进程间相互独立进程,进程之间不能共享资源,一个进程至少有一个线程,同一进程的各线程共享整个进程的资源(寄存器、堆栈、上下文),但是每个线程中的资源不会进行线程间共享。
3、线程的创建和切换开销比进程小。
多线程的实现有两种方式:第一种方式是继承 Thread 类,然后重写 run 方 法;第二种是实现 Runnable 接口,然后实现其 run 方法。
public Thread() :分配一个新的线程对象。
public Thread(String name) :分配一个指定名字的新的线程对象。
public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
public Thread(Runnable target,String name) : 分配一个带有指定目标新的线程对象并指定名字。
常用方法:
public String getName() :获取当前线程名称。
public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
public void run() :此线程要执行的任务在此处定义代码。
public static void sleep(long millis) : 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
public static Thread currentThread() :返回对当前正在执行的线程对象的引用。 yield() :暂停当前正在执行的线程对象,并执行其他线程。 join() : 等待该线程终止。
继承Thread 类后,然后重写run方法(称为线程体),通过调用Thread类的start()方法来启动一个线程。
public class TestRun { public static void main(String[] args) { Mytread mytread=new Mytread(); mytread.start();//启动线程 } } //线程类 class Mytread extends Thread{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(this.getName()+"--"+i); } } }注意:如果我们的类已经继承了一个类(如小程序必须继承自 Applet 类),则无法再继承 Thread 类。
这种方式克制了单继承Thread 类的缺点
实现 Runnable 接口,然后实现其 run 方法。
public class TestRun { public static void main(String[] args) { Mytread mytread=new Mytread(); Thread thread=new Thread(mytread); thread.start();//启动线程 } } //线程类 class Mytread implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+"--"+i); } } }总结:两种生成线程对象的区别:
1、这两种方法都需要执行start方法为线程分配必须的系统资源、调度线程运行并执行线程的run方法。
2、具体需要用到那种方式,还得看具体的需求。
新生(新建)状态:
当用new操作符创建一个新的线程对象时,该线程处于新生状态。
处于创建状态的线程只是一个空的线程对象,系统不为它分配资源 。
就绪状态:
处于就绪状态的线程已经具备运行条件了,还需要等待系统给它分配CPU,就绪状态不是执行状态,可以把它理解为等待状态,线程进入运行状态后就会自动调用自己的run方法,有4中原因会导致线程进入就绪状态:
新建线程:调用start()方法后,进入就绪状态 阻塞线程:阻塞解除后,进入就绪状态 运行线程:调用yield()方法暂停线程时,进入就绪状态 运行线程:JVM将CPU资源从本线程切换到其他线程时,线程需要等待。运行状态:
在运行状态时线程在执行自己的run方法中的代码,直到调用其它的终止方法或者等待某些资源里阻塞或者完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。
阻塞状态:
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。有4种原因会导致阻塞:
1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。
2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。
3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。
4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。
死亡状态:
线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作; 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(注:stop() / destroy()方法已经被JDK废弃,不推荐使用)。
当一个线程进入死亡状态以后,就不能再回到其它状态了。
总结:线程状态可以分为5个状态:新建状态、就绪状态、运行状态、阻塞状态、死亡状态。
获取线程基本信息的方法
1. 处于就绪状态的线程,会进入“就绪队列”等待JVM来挑选。
2. 线程的优先级用数字表示,范围从1到10,一个线程的缺省优先级是5。
3. 使用下列方法获得或设置线程对象的优先级。
int getPriority();//获取优先级 void setPriority(int newPriority);//更改线程的优先级注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高的线程后调用优先级低的线程。(也就是说优先级高的获得调度的概率高,优先级低的获得的概率低)。
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。需要用到的关键字:synchronized
它包括两种用法:synchronized 方法和 synchronized 块。
synchronize 方法
public synchronized void accessVal(Railway railway);synchronize 块
synchronized(syncObject){ //允许访问控制的代码 }死锁是由于“同步块需要同时持有多个对象锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁。好比如有甲和乙两个人一起去饭堂吃饭,刚好桌子上有一双筷子、一把刀和一把叉,其中甲拿了一根筷子和一把刀,另乙拿了一把叉和一根筷子。之后甲跟乙说:“你先把你手中的那根筷子给我,我再把刀给你”,乙跟甲说:“你先把你手中的那把刀给我,我再把筷子给你”,双方都不肯让,之后谁也吃不了饭。直接造成死锁。
死锁例子:
public void run() { System.out.println(a); if(a == 0){ try { synchronized (TestSync.o1) { //已获取o1对象锁 Thread.sleep(500); System.out.println("Try to get o2"); synchronized (TestSync.o2) { //未释放o1,尝试获取o2,被挂起 System.out.println("aaaa"); } } } catch (InterruptedException ex) { } } else { try { synchronized (TestSync.o2) { //已获取o2的对象锁 Thread.sleep(500); System.out.println("Try to get o1"); synchronized (TestSync.o1) { //未释放o2,尝试获取o1,也被挂起,死锁 System.out.println("aaba"); } } } catch (InterruptedException ex) { } } }Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock() :加同步锁。 public void unlock() :释放同步锁。 class Test implements Runnable{ private int ticket=100; Lock lock=new ReentrantLock(); @Override public void run() { while(true) { //获取锁 lock.lock(); if(ticket>0) { //出票 try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //获取当前线程对象的名字 String name = Thread.currentThread().getName(); System.out.println(name+"正在卖:"+ticket--); } lock.unlock(); } } }多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。
1、什么是生产者?
生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
2、什么是消费者?
消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。
3、什么是缓冲区?
消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。
缓冲区是实现并发的核心,缓冲区的设置有3个好处:
1、实现线程的并发协作
有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。
2、解耦了生产者和消费者(解掉了生产者和消费者之间的耦合)
生产者不需要和消费者直接打交道。(也就是说切断了生产者和消费者的直接联系)
3、解决忙闲不均,提高效率
生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据。
线程并发协作总结:
线程并发协作(也叫线程通信),通常用于生产者/消费者模式,运行情景如下:
生产者和消费者共享同一个资源区,生产者与消费者相互依赖,互为条件。 对于生产者来说,在没有生产产品之前,消费者需要进入等待状态(就绪状态)。而在生产了产品之后又要需要马上通知消费者消费。 对于消费者来说,当消费者消费了产品后,需要通知生产者继续生产新的产品提供消费。 在生产者消费者问题中,仅有synchronized是不够的。· synchronized可阻止并发更新同一个共享资源,实现了同步;
· synchronized不能用来实现不同线程之间的消息传递(通信)。
线程的常用的通讯方法
java.util.Timer
Timer类作用是类似闹钟的功能,也就是个定时器,Timer类本身实现的就是一个线程,只是这个线程是用来实现调用其它线程的。(一般用于调用线程)
java.util.TimerTask
TimerTask类是一个抽象类,该类实现了Runnable接口,所以该类具备多线程的能力。
public class TestThread5 { public static void main(String[] args) { Timer t1=new Timer();//定义定时器 MyTask task=new MyTask();//定义任务 //t1.schedule(task, 3000);//3秒后执行 //t1.schedule(task, 5000,1000);//五秒后执行,每隔一秒执行一次 GregorianCalendar calendar1 = new GregorianCalendar(2019,9,31,16,46,20); t1.schedule(task,calendar1.getTime()); //指定定时时间; } } class MyTask extends TimerTask{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("任务:"+i); } } }注意:Timer一般配合着TimerTask来使用。
其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源。
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads) : //返回线程池对象。(创建的是有界线 程池,也就是池中的线程个数可以指定最大数量,创建线程池是必须指定线程的数量)
获取到了一个线程池ExecutorService 对象,那么怎么使用呢,使用线程池对象的方法如下:
public Future submit(Runnable task) :获取线程池中的某一个线程对象,
并执行 Future接口:
用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程的步骤:
1、先创建好线程池对象(创建线程时,必须定义线程的数量)
ExecutorService es= Executors.newFixedThreadPool(2);//包含两条线程对象
2、创建Runnable接口子类对象。
MyRunnable runnable=new MyRunnable();
3、提交Runnable接口子类对象
es.submit(runnable);//从线程池中获取线程对
4、关闭线程池(一般不做)。
es.shutdown();//关闭线程
测试类:
public class ThreadPoolDemo { public static void main(String[] args) { //1、创建线程池对象(创建线程时,必须定义线程的数量) ExecutorService es= Executors.newFixedThreadPool(2); //2、创建Runnable接口子类对象 MyRunnable runnable=new MyRunnable(); //3、从线程池中获取线程对象,然后调用MyRunnable中的run() es.submit(runnable); // 再获取个线程对象,调用MyRunnable中的run() es.submit(runnable); es.submit(runnable); // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。 // 将使用完的线程又归还到了线程池中 //4、关闭线程池 //es.shutdown(); } } class MyRunnable implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("游泳"+i); } } }注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中,只有关闭了线程池里面的线程才会真正的进入死亡状态。