一. 多线程概述
1. 进程和线程的概念及区别
进程: 进程是一个正在运行的程序。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。
线程: 一个程序内部的顺序控制流,也可以说是进程中的一个独立的控制单元。线程在控制着进程的执行。
①只要进程中有一个线程在执行,进程就不会结束。②一个进程中至少有一个线程。
2. 多线程概述及初步理解
在java虚拟机启动的时候会有一个java.exe的执行程序,也就是一个进程。该进程中至少有一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法
中。该线程称之为主线程。JVM启动除了执行一个主线程,还有负责垃圾回收机制的线程。这种在一个进程中有多个线程同时执行的方式,就叫做多线程。
3. 多线程存在的意义
多线程的出现能让程序产生同时运行效果。可以提高程序执行效率。
例如:在java.exe进程执行主线程时,如果程序代码特别多,在堆内存中产生了很多对象,而同时对象调用完后,就成了垃圾。如果垃圾过多就有可能是堆内存出现内存
不足的现象,只是如果只有一个线程工作的话,程序的执行将会很低效。而如果有另一个线程帮助处理的话,如垃圾回收机制线程来帮助回收垃圾的话,程序的运行将变得
更有效率。
二. 创建线程的方式
创建线程的方式主要有两种:1. 继承Java.lang.Thread类 2. 实现runnable接口,下面具体说明:
1. 继承Java.lang.Thread类
通过继承Thread类,然后复写其run方法的方式来创建线程
实现步骤: a.定义类继承Thread → b.重写Thread类中的run方法 → c. 创建定义该类的实例对象,调用start方法开启线程
注:如果对象直接调用run方法,等同于在主线程中执行普通方法run(),自定义的线程并没有启动。
2. 实现runnable接口方式
使用继承方式有一个弊端,那就是如果该类本来就继承了其他父类,那么就无法通过Thread类来创建线程了。
这样就有了第二种创建线程的方式:实现Runnable接口,并重写其中run方法的方式。
实现步骤: a.定义类实现Runnable接口 → b.重写runnable接口中的run方法 → c.通过Thread类创建线程对象,将Runnable接口的子类对象作为实参传递给Thread类的构造函数→ d 调用Thread类的
start方法开启线程
实现方式实现的好处: 避免了单继承的局限性。
两种方式开启线程的区别:
① 线程代码存放的位置不一样,继承Thread方式:存放在Thread子类中 而实现Runnable方式:存放在runnable接口的子类中
② 实现runnable方式开启线程能避免单继承的局限性
③ 实现runnable方式开启因为线程代码存放位置,能将一些资源独立出来,比如下面例子的票号,就不需要加static静态修饰
package create; /* * 需求:简易的多窗口卖票程序 * 总票数100张,从100票号开始卖,卖出一张票票号减1 */ public class TicketDemo{ static class Ticket implements Runnable{ private static int tick = 100; //设置起始票号 @Override public void run() { while(true){ if(tick>0){ System.out.println(Thread.currentThread().getName()+"号线程售出:"+tick-- +"号票"); } } } } public static void main(String[] args) { //创建Runnable接口子类的实例对象 Ticket t = new Ticket(); //有多个窗口在同时卖票,这里用四个线程表示 Thread t1 = new Thread(t);//创建了一个线程 Thread t2 = new Thread(t); Thread t3 = new Thread(t); Thread t4 = new Thread(t); t1.start();//启动线程 ,模拟多个窗口开始卖票 t2.start(); t3.start(); t4.start(); } }
三. 线程的几种状态及演变
① 新建状态(New)
当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码
② 就绪状态(Runnable)
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。start()
方法返回后,线程就处于就绪状态。
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有
一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
③ 运行状态(Running)
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.
④ 阻塞状态(Blocked)
线程在执行过程中,可能由于各种原因进入阻塞状态:
1. 调用sleep()方法进入睡眠状态
2. 线程试图得到一个锁,而该锁正被其他线程持有
3. 线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者(不怎么会用)
......
注意:处于阻塞状态的线程可以通过notify()方法激活到就绪状态
⑤ 死亡状态(Dead)
通过stop(),interrupt()方法强制结束线程,或者执行完run()方法结束
为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡
了,则返回false
状态转换图:
四. 多线程安全问题
多线程在提高效率的同时,如果操作不当,就可能带来安全隐患,具体表现如下:
1. 导致安全问题的出现的原因
当多条语句在操作同一线程共享数据时,一个线程对多条语句只执行了一部分,还没用执行完,另一个线程参与进来执行。导致共享数据的错误
简单来说,原因有两点:① 多个线程访问出现延迟
② 线程随机性
2. 解决办法——同步
同步就是当多个线程操作共享数据时,只能让一个线程都执行完,在执行过程中,当其他线程涉及该共享数据时,只能等待。
同步的关键字是---synchronized(同步)
3. 同步的两种方式: ① 同步代码块 ② 同步函数
① 同步代码块
用法:
synchronized(对象){
需要被同步的代码;
}
注意:同步可以解决安全问题的根本原因就在那个对象上。其中对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也无法金进入
② 同步函数
public synchronized void show(){
需要同步的语句;
}
我们知道,同步需要有一个锁,也就是对象,那么同步函数用的是哪个锁呢?
函数需要被对象调用。那么函数都有一个所属对象引用。就是this。所以同步函数使用的锁是this。
4. 同步的前提
a,必须要有两个或者两个以上的线程。
b,必须是多个线程使用同一个锁。
利用这两个性质,可以判断如果符合这两个条件,就不会出现安全问题!
注意:如果同步函数被静态修饰后,使用的锁该类对应的字节码对象。 如:类名.class 该对象的类型是Class
五. 死锁
当同步中嵌套同步时,线程占有一个锁对象,进而请求另一个锁对象,循环下去--就有可能出现死锁现象
例子:
package create; /** * 死锁小例子 * @author J */ public class DeadLock implements Runnable { // 静态对象是类的所有对象共享的 private static Object o1 = new Object(), o2 = new Object(); private boolean flag; // 标志,用于区分进入进程的不同同步区域 DeadLock(boolean flag) { this.flag = flag; } @Override public void run() { if (flag) { synchronized (o1) { System.out.println(Thread.currentThread().getName() + "------if_locka"); synchronized (o2) { System.out.println(Thread.currentThread().getName() + "------if_lockb"); } } } else { synchronized (o2) { System.out.println(Thread.currentThread().getName() + "------if_lockb"); synchronized (o1) { System.out.println(Thread.currentThread().getName() + "------if_locka"); } } } } public static void main(String[] args) { // 创建2个进程,并启动 new Thread(new DeadLock(true)).start(); new Thread(new DeadLock(false)).start(); } }
六. 线程间通信
当多个线程并发执行时,在默认情况下CPU是随机切换线程的。如果我们希望他们有规律的执行, 就可以使用通信, 例如两个线程执行一次打印操作,一个线程输入数据完成后,另一个线程取数据打印
具体实现: 等待唤醒机制,即:① 如果希望线程等待,就调用wait() ② 如果需要唤醒等待的线程,就调用notify方法.
注意: wait(),notify(), notifyAll()这三个方法都需要使用在同步中,因为同步才拥有锁,而这些方法用于对持有同一个监视器(锁)的线程进行操作
换言之,在同一个锁上wait的对象只能被同一个锁上的notify唤醒
而锁是任意对象,所以这三个方法定义在object类上
例子: 使用同步操作同一资源
package create; /** * 使用同步使用同一资源,并且两个线程协调进行 即: 产生一个就使用一个 * * @author J * */ public class ResouseDemo { /* * 资源:包含 姓名,性别,以及一个标志位 */ static class Resource { private String name; private String sex; private boolean flag; public synchronized void setInput(String name, String sex) { if (flag) { try { wait(); } catch (Exception e) { } } this.name = name; this.sex = sex; flag = true; notify();// 唤醒等待 } public synchronized void getOutput() { if (!flag) { try { wait(); } catch (Exception e) { }// 如果木有资源,等待存入资源 } System.out.println(name + sex); flag = false; notify(); // 将输入线程唤醒 } } static class Input implements Runnable { private Resource r; public Input(Resource r) { this.r = r; } @Override public void run() { int x = 0; // 循环交替输入资源 while (true) { if(x==0){ r.setInput("韩雷雷",".....男"); }else{ r.setInput("李东东",".....女"); } x = (x+1)%2; } } } static class Output implements Runnable { private Resource r; public Output(Resource r) { this.r = r; } // 循环交替打印输出 @Override public void run() { while (true) { r.getOutput(); } } } public static void main(String[] args) { Resource r = new Resource(); // 定义一个资源 new Thread(new Input(r)).start();// 开启存线程 new Thread(new Output(r)).start();// 开启取线程 } }
几个小问题??
1. wait(),notify(),notifyAll(),用来操作线程为什么定义在了Object类中?
a. 这些方法存在与同步中。
b. 使用这些方法时必须要标识所属的同步的锁。同一个锁上wait的线程,只可以被同一个锁上的notify唤醒。
c. 锁可以是任意对象,所以任意对象调用的方法一定定义Object类中。
2. wait(),sleep()有什么区别?
wait():释放cpu执行权,释放锁。
sleep():释放cpu执行权,不释放锁。
3. 为甚么要定义notifyAll?
因为在需要唤醒对方线程时。如果只用notify,容易出现只唤醒本方线程的情况。导致程序中的所以线程都等待。
注意: 当有超过两个线程进行通信时,修改如下:1.因为多个线程,所以需要用notifyAll() 2. if判断改为while判断,因为唤醒之后要重新判断标记
2. JDK1.5以后线程通信解决方案(重要)
JDK1.5以后使用ReentrantLock类的lock()和unlock()方法代替进行同步synchronized进行同步
使用步骤: ① Lock lock=new ReentrantLock()得到lock对象,在需要同步的多个线程中用lock()和unlock()代替synchronized用于同步
② Condition condition=lock.newCondition()得到condition对象,用condition.await()和condition.signal()等待和唤醒线程
注意: 一个lock对象可以有多个condition对象,用与分别控制不同线程的等待和唤醒,这是JDK1.5以后新特性的明显好处
不同的线程使用不同的Condition, 这样就能区分唤醒的时候找哪个线程了
升级解决方案的示例:生产者与消费者问题
import java.util.concurrent.locks.*; /* * 用JDK1.5以后新特性解决 * 生产者与消费者问题,商品总个数为不超过10个 */ public class JDK5XinTeXing { static class Resource { private String name; private int conut = 0; private Lock lock = new ReentrantLock(); // 接口具体实现类(多态) Condition condition_pro = lock.newCondition(); Condition condition_con = lock.newCondition(); public void setProducer(String name) throws InterruptedException { lock.lock(); // 上锁 try { while (conut >= 10) { // 生产超过10个,唤醒消费者消费 condition_con.signal(); condition_pro.await(); } this.name = name; conut++; System.out.println(Thread.currentThread().getName() + "生产1个" + this.name + ",总个数:" + conut);// 打印生产 } finally { lock.unlock();// 解锁,这个动作一定执行 } Thread.sleep(1000); } public void getConsumer() throws InterruptedException { lock.lock(); try { while (conut <= 0) { // 商品数目只有0个,唤醒生产者 condition_pro.signal(); condition_con.await(); } conut--; System.out.println(Thread.currentThread().getName() + "消费1个" + this.name + ",总个数:" + conut);// 打印消费 } finally { lock.unlock(); } Thread.sleep(1000); } } // 生产者线程 static class Producer implements Runnable { private Resource res; Producer(Resource res) { this.res = res; } @Override public void run() { while (true) { try { res.setProducer("牛奶"); } catch (InterruptedException e) { } } } } // 消费者线程 static class Consumer implements Runnable { private Resource res; Consumer(Resource res) { this.res = res; } // 复写run public void run() { while (true) { try { res.getConsumer(); } catch (InterruptedException e) { } } } } public static void main(String[] args) { Resource res = new Resource(); new Thread(new Producer(res)).start();// 第一个生产线程 p1 new Thread(new Consumer(res)).start();// 第一个消费线程 c1 new Thread(new Producer(res)).start();//第二个生产线程 p2 new Thread(new Consumer(res)).start();//第二个消费线程 c2 } }
运行结果图:
七. 停止线程
在JDK 1.5版本之前,有stop停止线程的方法,但升级之后,此方法已经过时。
那么现在我们该如果停止线程呢?———只有一种办法,那就是让run方法结束。
1. 线程中run()方法中的运行代码一般都是循环结构,只要让循环结束 —> run()方法就结束 —> 线程就结束了
一般做法是设置一个flag标记,并在线程代码中给出修改标记的实现方法
如:
class a implements Runnable{ private boolean flag = true; @Override public void run() { while(flag){ System.out.println(Thread.currentThread().getName()+"....run"); } } public void changeFlag(){ flag = false; }
2. 有一种特殊情况:即线程因为某些原因处于冻结状态run方法无法结束,那么线程就不会结束。
比如:同步的wait()方法,Thread.sleep()方法使线程挂起,这是就需要对冻结进行清除,强制让线程恢复到运行状态中来
要做到我们可以使用: Thread类提供该方法interrupt() .
注意使用点: interrput()使用后会一砖头拍下去到catch(InterrpuException e)(接受包扎),在catch代码块中要对标记进行改变
class Test implements Runnable { private boolean flag = true; public synchronized void run() { while (flag) { try { wait(); } catch (InterruptedException e) { System.out.println("分线程进入异常---"); flag=false; //打断之后要改变标记使线程停止 } System.out.println("分线程进入主循环---"); } } public static void main(String[] args) { Test t = new Test(); Thread thread = new Thread(t); thread.start(); thread.interrupt(); } }
八. 线程中的其他一些方法讲解
1. SetDaemon(boolean on) 将该线程标记为用户线程(false)或者守护线程(true) ,
守护雅典娜:雅典娜是主线程,如果其他线程都是守护线程,雅典娜运行完了,其他线程就结束了
① 当正在运行的线程都是守护线程时,Java虚拟机自动退出
② 该方法必须在启动线程前使用
2. public final void join() throws InterrupttedException
说明:在A线程中遇到B.join()时
① A将释放CPU执行权利 ② 只有等到B线程执行完,A线程才开始继续执行 ③ Join可以用于在主线程中临时加入线程执行
3. setPriority(int newPriority) 设置优先级,优先级高的容易得到CPU执行权,默认值为5
MAX_PRIORITY 最高优先级10
MIN_PRIORITY 最低优先级1
NORM_PRIORITY 分配给线程的默认优先级
4. public static void yield() ---- Thread.yield()
可以暂停当前线程,让其他线程执行
九. 什么时候用多线程?
当某些代码需要同时被执行时,就用单独的线程进行封装。