我们知道创建线程有两种方法:继承Thread类,和实现Runnable接口,有什么区别呢?
首先,我们要回顾一下Java线程的知识:
第三部分 多线程编程
1. Java线程模型
- 线程是进程中的内容,是程序中的执行路径,或者叫一个控制单元;线程在控制着进程的执行;
- Java VM启动时会有一个进程java.exe;其中至少有一个线程负责java程序的运行,该线程存在于main()方法中,称之为主线程;
- 实际上,jvm默认启动的还有负责垃圾回收机制的线程;
- 主线程被jvm开启,其余的线程需要在子线程中开启;
- 多个线程实际上是不能同时执行的(和进程相同),在多个线程之间随机(优先级相同的情况下)切换;在某一个时刻,只有一个进程中的某一个线程在运行(多核CPU不同),线程间切换具有随机性;
1.1 线程状态
- 被创建
- 运行(running)
- 挂起(suspend)、冻结,阻塞(block)、临时状态:
注意理解具备执行资格和具备执行权的区别!
- 具备执行资格,但是放弃了执行权
- sleep(time);
- wait();
- *恢复(resume):恢复到临时状态(没有执行权)或者到运行状态(具备执行权)
- notify();
终止(terminate)、消亡:
- stop();
- run()方法结束;
注意:suspend() stop() resume()在新特性中均已过时。
1.2 特点
- 优先级:线程的优先级用来决定何时从一个运行的线程切换到另一个,即“上下文转换”(context switch);
- 同步性:管程(monitor)或者理解为锁(Lock类),在进程同步性的老模式基础上实行的另一种方法,防止共享的资源被多个线程操纵;
- 消息传递:多线程间通信通过调用所有对象都有的预先确定的方法;
- Thread类和Runable接口:
- getName 获得线程名称(setName 改变线程名称);
- getPriority 获得线程优先级;
- isAlive 判断进程是否仍在运行;
- join 等待一个线程终止;
- run 线程的入口点;
- sleep 在一段时间内挂起线程,延迟1ms;
- start 通过调用运行方法来启动线程;
2. 创建线程
2.1 主线程
- 主线程在程序启动时自动创建,由一个Thread对象控制,调用其currentThread()方法(Thread类的公有静态成员)获得主线程的引用:
static Thread currentThread()
- 调用主线程示例:
class CurrentThreadDemo{
public static void main(String[] args){
Thread t = Thread.currentThread();
System.out.println("Current thread: " + t);
t.setName("BJTShang’s Thead");
System.out.println("After name changed: " + t);
try{
for(int n=5;n>0;n--){
System.out.println(n);
Thread.sleep(1000);
}
}catch(InterruptedException e){
System.out.println("Main thread interruped");
}
}
}
输出:
Current thread: Thread[main,5,main]
After name changed: Thread[BJTShang’s Thead,5,main]
5
4
3
2
1
在本程序中,对主线程的引用通过调用currentThread()方法获得,用局部变量t保存该引用;通过调用Thread类的各子方法setName,sleep控制主线程。线程信息输出格式:Thread[线程名称,优先级,组名称]
2.2 创建线程(重点)
通过实例化一个Thread对象来创建新线程:继承Thread类,或者实现Runnable接口;
1. 继承Thread类
重写新线程的入口run()方法,调用start方法启动新线程;(通常主线程必须是结束运行的最后一个进程:保证主线程运行足够时间)
1. seName()getName()方法:默认名称“Thread-编号”;
2. 也可以构造使用super调用Thread父类带字符串参数构造方法的构造方法;
3. ```
class Sub extends Thread{
Sub(String name){
super(name);//调用父类Thread的构造方法,在创建Sub类对象时,自定义线程名称
}
}
```
class SubThread extends Thread{
public void run(){
System.out.println("SubThread Run");
}
public static void main(String[] agrs){
SubThread st = new SubThread();//创建好一个线程;
st.start();//start()方法两个作用:启动该线程对象,调用该对象中的run()方法;
//st.run();main方法中的主线程调用run方法,但是没有启动SubThread线程;依然属于单线程程序;
}
}
Thread类中的run方法,用于存储线程要运行的方法(主线程要运行的代码存放于main方法)
卖票被卖出多张的问题:
class Ticket extends Thread{
private int ticket = 100;
//private static int ticket = 100; 将ticket设置成静态,可以防止同一张票被卖出多张:相同的数据(实际上每个线程在内存中数据的位置是不同的(每一个线程都单独开辟了一块内存区域),是不同的数据,只是数值相同)被多个线程共享
//如果定义成static,则实现了与类的静态绑定,避免数据被共享,但是:static的生命周期太长,占用资源
public void run(){
while (true){
if (ticket > 0)
{
System.out.println(currentThread().getName() + "..." + ticket--);
}
}
}
public static void main(String[] args)
{
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
Ticket t4 = new Ticket();
t1.start();
t2.start();
t3.start();
t4.start();
}
}
2. 实现Runnable方法
1. 定义类实现Runnable接口;
2. 覆盖Runnable接口中的run方法;
* 将线程要执行的代码存放在该run方法中
3. 通过Runnable子类对象,创建Thread类线程对象;
4. 将Runnable接口子类对象作为参数传递给Thread类的构造函数;
* 因为要创建的线程所执行的代码run方法属于Runnable接口的子类对象,如果想让新创建的线程去执行run方法中的代码,必须将其所属对象传递给新线程;
5. 调用Thread类的start方法开启线程并调用Runnable接口子类中实现的run方法;
3. 那么,继承Thread类和实现Runnable接口创建新线程有什么区别?
- 避免了单继承的局限性,因此通常使用实现Runnable的方式创建多线程;
- 同时,由于使用的是将run方法所属对象传给Thread对象,因此,当多个线程使用同一个run方法时,可以传入同一个对象,而不会发生数据冲突(因为在内存中只有一个run方法所属对象,多个线程共享同一块内存区域),但同时也会因为抢夺共享的数据,而引起安全问题,因此,下面要介绍的线程同步非常重要;
class Ticket implements Runnable{
private int ticket = 100;
public void run(){
while (true){
if (ticket > 0)
{
System.out.println(Thread.currentThread().getName() + "..." + ticket--);
}
}
}
public static void main(String[] args)
{
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();
}
}
3. 线程同步,线程锁
两个或两个以上的线程需要共享资源,需要一种办法来确定资源在某一时刻仅被一个线程占用,否则会出现安全问题,例如:
class Ticket implements Runnable{
private int ticket = 10;
public void run(){
while (true){
if (ticket > 0){
try{
Thread.sleep(300);//此时间内,多个线程可以同步进入run方法,出现负票
}catch (Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "..." + ticket--);
}
}
}
public static void main(String[] args){
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();
}
}
- 共享数据安全性:
- 核心思想就是:当a线程执行一段代码时(这段代码中存在对数据的改变操作),别的线程必须等待a线程执行完,才可以执行这段代码。
- 同步的关键是“锁”的概念,JDK新特性中甚至加入了Lock类,稍后将对此说明;
必须清楚共享数据在哪里使用;
使用synchronized关键字:进入某一对象的管程,就是调用被synchronized关键字修饰的方法;在此同步方法内部,所有试图调用该方法的同实例的其他线程必须等待;直到拥有管程的线程从同步方法中退出,就可以将对象(锁)的控制权放弃给其他等待的线程;那些等待的线程被放在线程池中,并根据等待的先后顺序依次被放入管程中。
- 使用同步方法:eg. synchronized void call(String msg)
- 使用同步代码块,将对这个类定义的方法的调用放入一个synchronized块: synchronized(object){};
- 一个同步块确保每个线程运行前都等待先前的一个线程结束;
- 同步方法用的是什么锁?使用的锁是this,如果使用不同的锁,会出安全问题;
- 同步代码块使用的是什么锁?使用的锁是object对象锁,可以使用任意类的对象,因为任意对象都有对象锁;
- 如果同步方法被static修饰,使用的锁不是this(静态方法中不能有this),而是类名.class的Class类对象;
单例设计模式,总结:
线程死锁:避免
* 出现:同步中嵌套同步;
class Test implements Runnable{
private boolean flag;
Test(boolean flag){
this.flag = flag;
}
public void run(){
if(flag){
synchronized(MyLock.locka){
System.out.println("if locka");
synchronized(MyLock.lockb){
System.out.println("if loakb");
}
}
}
else{
synchronized(MyLock.lockb){
System.out.println("else lockb");
synchronized(MyLock.locka){
System.out.println("else locka");
}
}
}
}
}
class MyLock
{
static Object locka = new Object();
static Object lockb = new Object();
}
class DeadLockTest
{
public static void main(String[] args)
{
Thread t1 = new Thread(new Test(true));
Thread t2 = new Thread(new Test(false));
t1.start();
t2.start();
}
}
4 线程间通信
多个线程在操纵一个资源
class Res{
String name;
String gender;
}
class Input implements Runnable{
private Res r;
Input(Res r){
this.r = r;
}
public void run(){
int x = 0;
while (true){
if(x==0){
r.name = "晨晨";
r.gender = "男的";
}else{
r.name = "红题";
r.gender = "女的";
}
x = (x+1)%2;
}
}
}
class Output implements Runnable{
private Res r;
Output(Res r){
this.r = r;
}
public void run(){
while(true){
System.out.println(r.name + "..." + r.gender);
}
}
}
class ThreadCom{
public static void main(String[] agrs){
Res s = new Res();
Input i = new Input(s);
Output o = new Output(s);
Thread t1 = new Thread(i);
Thread t2 = new Thread(o);
t1.start();
t2.start();
}
}
Input生产数据的同时,Output在消费数据,不安全。解决方法:给两个线程加上同一个锁;
class Res{
String name;
String gender;
}
class Input implements Runnable{
private Res r;
Input(Res r){
this.r = r;
}
public void run(){
int x = 0;
while(true){
synchronized(r){
if(x==0){
r.name = "BJT";
r.gender = "男的";
}
else{
r.name = "Shenqidemao";
r.gender = "女的";
}
x = (x+1)%2;
}
}
}
}
class Output implements Runnable{
private Res r;
Output(Res r){
this.r = r;
}
public void run(){
while(true){
synchronized(r){//两个线程使用同一个锁,对象相同就行,r对象引用也可以换成Input.class
System.out.println(r.name + "..." + r.gender);
}
}
}
}
class ThreadCom{
public static void main(String[] agrs){
Res s = new Res();
Input i = new Input(s);
Output o = new Output(s);
Thread t1 = new Thread(i);
Thread t2 = new Thread(o);
t1.start();
t2.start();
}
}
等待唤醒机制
* 远离轮询:经典的序列问题:一个线程正在生产数据而另一个线程在使用、消费数据,在轮询系统中,消费者在等待生产者生产数据时浪费CPU周期,而生产者也必须等待消费者工作结束才能继续启动,浪费更多CPU时间;
* wait(),notify(),notifyAll()方法实现线程间通信
- 仅在synchronized方法中才能被调用,因为要对持有监视器(锁)的线程操作
- wait():被调用的线程放弃管程,进入睡眠,放弃执行权,直到其他线程进入相同管程并调用notify(),这些wait()状态的锁按照先后顺序存放在线程池中;
- notify():恢复相同对象中(也就是使用同一个锁)第一个调用wait()的线程;
- notifyAll():恢复相同对象中所有调用wait()的线程;根据优先级判定运行顺序;
- 以上三个方法都定义在Object类中,因为这些方法在操纵同步中的线程时,都必须要标识该线程的锁,只有同一个锁上的睡眠,可以被同一个锁上的notify唤醒;而锁可以是任意对象,因此这些方法定义在Object类中;
5 理解消费者生产者模型,就可以理解多线程!
synchronized{
while(true){
wait();
}
...
false;
notifyAll();
}
- JDK1.5中提供了多线程升级解决方案
- 将同步synchronized替换成了现实的Lock操作;
- 将Object监视器中的wait,notify,notifyAll方法,替换成了Condition对象,该对象可以使用Lock锁进行获取,可以只唤醒Lock对象中的特定Condition对象;注意:释放锁的动作一定要执行,使用try{}finally{}
6 线程其他方法
6.1 isAlive()、join()、setDaemon()
- 一个线程如何知道另一线程已经结束?
- 在线程中调用isAlive()判定当前进程是否结束/Alive;
final boolean isAlive()
- 更常用:调用join()要求当前线程等待直到指定的线程参与;
final void join() throws InterruptedException
6.2 线程优先级
- 线程调度决定优先级高的线程获得更多CPU时间,当然,获得的CPU时间依然取决于别的因素(一个实行多任务处理的操作系统如何更有效的利用CPU时间);例如,当低优先级线程运行时,一个高优先级的线程被恢复(从沉睡中或者I/O等待中),高优先级的线程将抢占低优先级所使用的CPU;
- 设置线程优先级/获得当前的优先级设置:
final void setPriority(int level)
final int getPriority()
当涉及调度时,Java的执行可以有本质上不同的行为,获得跨平台的线程行为的方法是自动放弃对CPU的控制; - 示例:两个不同优先级的线程,运行与具有优先权的平台
//Copyright @BJTShang
class clicker implements Runnable{
int click = 0;
Thread t;
private volatile boolean running = true;
public clicker(int p){
t = new Thread(this);
t.setPriority(p);
}
public void run(){
while(running){
click++;
}
}
public void start(){
t.start;
}
public void stop(){
running = false;
}
}
class HiLoPri{
public static void main(String[] args){
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
clicker hi = new clicker(Thread.NORM_PRIORITY + 2);
clicker lo = new clicker(Thread.NORM_PRIORITY - 2);
lo.start();
hi.start();
try{
Thread.sleep(10000);
}catch (InterruptedException e){
System.out.println("Main thread interrunpted.");
}
lo.stop();
hi.stop;
try{
hi.t.join();
lo.t.join();
}catch(InterruptedException e){
System.out.println("InterruptedException caught");
}
System.out.println("Low-priority thread: " + lo.click);
System.out.println("High-priority thread: " + hi.click);
}
}
running前的关键字volatile用来确保running的值在下面的循环中每次都得到验证;
6.3 stop()、suspend()、resume()方法已过时
- 如何停止线程?run()方法结束;
- 多线程运行,运行代码通常是循环结构的;
- 控制住循环,就可以让run()方法结束,也就是线程结束;
- 当线程使用同步或者锁时需要注意线程有可能一直处于wait()状态,需要用interrupt()方法打断;