1.Thread类
1.1 常用方法
1.void start() 启动当前线程;调用当前线程的run()方法
2.void run() 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3.static Thread currentThread() 返回执行当前代码的线程
4.String getName() 获取当前线程的名字
5.void setName() 设置当前线程的名字
6.static void yield() 当前线程释放cpu的执行权
注:当前线程调用 yield()方法后释放CPU执行权,但是下一时刻有可能该线程继续被CPU执行
因为调用yield( )后当前线程由运行状态→就绪状态,就绪状态时等待CPU调度(不同JVM有不同的调度算法)
所以下一刻可能CPU会继续被分配到当前线程,则当前线程又转变为运行状态
7.final void join() 在线程a中调用线程b的join()方法,此时线程a就进入阻塞状态,直到线程b完全执行完毕以后,线程a才结束阻塞状态继续执行。
8.static void sleep(long millitime) 让当前线程“睡眠”指定的millitime毫秒。睡眠期内当前线程是阻塞状态。
9.final boolean isAlive()判断当前线程是否存活
1.2 代码示例
// 测试yield()方法
class MyThread extends Thread
{
@Override
public void run()
{
for(int i = 0;i<5;i++)
{
if(i==2)
this.yield();//如果i==2则当前线程让步
System.out.println(getName()+":"+i);
}
}
}
public static void main(String[] args)
{
MyThread mt=new MyThread();
mt.start();
for(int i=0;i<5;i++)
System.out.println(Thread.currentThread().getName()+":"+i);
}
main:0
main:1
Thread-0:0
Thread-0:1
main:2
main:3
Thread-0:2 //i==2当前线程让步
Thread-0:3 //Thread-0 又分配到CPU继续被执行
main:4
Thread-0:4
//测试join()方法
class MyThread extends Thread
{
@Override
public void run()
{
for(int i = 0;i<5;i++)
{
System.out.println(getName()+":"+i);
}
}
}
public static void main(String[] args)
{
MyThread mt=new MyThread();
mt.start();
for(int i=0;i<5;i++)
{
if (i == 2)
{
try
{
mt.join(); // 在主线程中调用mt线程的join()方法则主线程被阻塞
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
main:0
main:1
Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
main:2//主线程执行到i=2后阻塞,Thread-0线程执行完毕后才继续执行
main:3
main:4
2.多线程的创建
2.1创建方式1:继承Thread类
(1) 创建继承于Thread类的子类
(2) 重写Thread类的run() 方法 (将此线程执行的操作声明在run()方法体内)
(3) 创建Thread类的子类的对象
(4) 通过此对象调用start()
class MyThread extends Thread // 1.创建Thread类的子类——MyThread
{
@Override
public void run() //2.重写run()方法
{
for(int i = 0;i<5;i++)
System.out.println(getName()+":"+i);
}
}
public static void main(String[] args)
{
MyThread mt1=new MyThread(); //3.创建Thread类的子类对象
mt1.start(); //4.调用start()方法
//若需要再次创建一个线程,则需要再创建另一个对象 ( 不能直接再次使用mt.start() )
MyThread mt2=new MyThread();
mt2.start();
}
//每次运行的结果可能不一样(请查看线程的生命周期)
Thread-0:0
Thread-0:1
Thread-1:0
Thread-0:2
Thread-1:1
Thread-0:3
Thread-1:2
Thread-0:4
Thread-1:3
Thread-1:4
程序运行情况图解(以上述代码为例)
2.2 Start()方法、run()方法
-
对象不能通过直接调用run( )方法启动线程。必须通过对象调用start( )方法
-
start( )方法作用:①启动当前线程 ② 调用当前线程的run()方法
-
start()方法的具体流程:
①创建线程对象,线程处于" 新建"状态
②线程对象调用start( )方法后线程处于"就绪"状态 (线程要等待CPU调度。不同的JVM有不同的调度算法,线程何时被调度是未知的。因此 start ( )方法的被调用顺序不能决定线程的执行顺序)
③调度成功后线程进入 “运行” 状态。
先检查线程对象是否被首次创建,如果不是则抛出" IllegalThreadStateException "异常。
如果线程对象首次被创建则回调run()方法(Java方法)执行线程的具体操作任务。 -
run( )方法:是一个普通方法。可以直接通过线程对象调用此方法
-
一个线程对象只能调用一次start()方法。如果重复调用则会抛出 " IllegalThreadStateException "异常
-
start( )方法被synchronized修饰,可以保证线程安全
-
由JVM创建的main方法线程和system组线程,并不会通过start来启动
-
start( )源码解析
public synchronized void start()
{
// 判断线程是否是首次创建,如果不是则抛出异常!
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 将线程添加到ThreadGroup中
group.add(this);
boolean started = false;
try
{
// 通过start0()启动线程,新线程会调用run()方法
start0();
// 设置started标记=true
started = true;
}
finally
{
try
{
if (!started)
{
group.threadStartFailed(this);
}
} catch (Throwable ignore) { }
}
}
public void run()
{
if (target != null)
{
target.run();
}
}
2.3 创建方式2:实现Runnable接口
(1) 创建一个实现了 Runnable 接口的类
(2) 类实现 Runnable 接口中的抽象方法:run()
(3) 创建类的对象
(4) 将此对象作为参数传递给Thread类的构造器以创建Thread类的对象
(5) 通过Thread类的对象调用start( )方法
class T_thread implements Runnable //1.创建T_thread类实现Runnable接口
{
private int num=5;
@Override
public void run() //2.重写run()方法
{
while(true)
{
if (num > 0)
{
System.out.println(Thread.currentThread().getName() + ":" + num);
num--;
}
else {
break;
}
}
}
}
public static void main(String[] args)
{
T_thread mt=new T_thread();//3.创建T_thread对象
Thread t1=new Thread(mt);//4.将T_thread对象作为参数传递给Thread构造器
t1.start(); //5.通过Thread对象调用start()方法
//若此时再创建一个线程:将mt作为参数传递给Thread构造器
//则此时t1,t2共用对象mt
//注:输出可能存在线程安全问题
Thread t2=new Thread(mt);
t2.start();
}
Thread-0:5
Thread-0:4
Thread-1:5
Thread-1:2
Thread-0:3
Thread-1:1
2.4关于创建方式2中start方法的说明
- Question:
我们已经知道start()方法的两个作用 ①启动当前线程 ② 调用当前线程的run()方法
创建方法2中的第4步创建的是Thread类的对象,第5步调用start()方法,那么根据start( )方法作用可知调用的是Thread类中的run( )方法,实现Runnable接口的实现类并未继承于Thread类(其中的run( )方法是实现,并不是重写),为何执行的时候会执行实现类的run()方法呢 ?
注:创建方式2不同于创建方式1。方式1创建了一个继承于Thread类的子类,子类重写了Thread类的run( )方法,再创建一个子类对象去调用start( )方法。根据重写定义不难知道此时start( )方法内调用的run( )方法是被重写的run( )方法 - Answer:
Thread类的对象调用的确实是Thread类的run( )方法。Thread t1=new Thread(mt) 调用了**public Thread(Runnable target)**构造器,将mt初始化为 !=NULL,那么回到run( )方法,此时就会执行target.run( ),即mt.run( )方法 (就是实现类中的run( )方法)
JDK源码如下
public void run()//Thread类中run()方法定义
{
if (target != null)
{
target.run();
}
}
private Runnable target;//Thread类中target定义
public Thread(Runnable target) //Thread类的一个构造器
{
this(null, target, "Thread-" + nextThreadNum(), 0);
}
2.5 创建方式3:实现Callable接口 ( JDK5新增 )
(1) 创建一个实现Callable接口的实现类
(2) 实现call方法:将此线程需要执行的操作声明在call( )方法中
(3) 创建实现类的对象
(4) 将实现类的对象作为参数传递到 FutureTask 类的构造器中以创建 FutureTask 的对象
(5) 将FutureTask的对象作为参数传递到Thread类的构造器中以创建Thread对象
(6) 通过Thread类的对象调用start( )方法
class Digit implements Callable //1.实现Callable接口
{
public int sum;
public Object call() throws Exception //2.实现call( )方法
{
for(int i=0;i<10;i++)
{
sum+=i;
System.out.println(Thread.currentThread().getName()+":"+i);
}
return sum; //自动装箱+多态
}
}
public static void main(String[] args)
{
Digit number=new Digit();//3.创建实现类的对象
FutureTask futureTask=new FutureTask(number); //4.将实现类作为参数传递给FutureTask构造器
Thread thread=new Thread(futureTask);//5.将FutureTask类的对象作为参数传递给Thread类的构造器
thread.start(); //6. Thread类的对象调用start( )方法
try
{
System.out.println(futureTask.get());//借助FutureTask类获取call( )返回结果
} catch (InterruptedException | ExecutionException e)
{
e.printStackTrace();
}
}
Thread-0:0
...
Thread-0:9
45
Callable比Runnable 更强大
-
相比run()方法,可以有返回值
-
方法可以抛出异常
-
支持泛型的返回值 ( 需要借助FutureTask类 )
2.6 创建方式4:线程池 ( JDK5新增 )
- 经常创建和销毁线程会使用量特别大的资源,对性能影响很大。提前创建好多个线程放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用
- 常见线程池API
1.ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor
2.Executors:工具类、线程池的工厂类。用于创建并返回不同类型的线程池
Executors.newCachedThreadPool() 创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n) 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() 创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
3.线程优先级
3.1 线程的调度
- 时间片:同优先级线程组成先进先出队列(先到先服务)
- 抢占式:对高优先级使用优先调度的策略
3.2 优先级等级
- 优先级等级在Thread 类中被定义为静态常量
public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;
3.3 相关方法
getPriority() 返回线程优先级
setPriority(int newPriority) 设置线程的优先级
3.4 两点说明
- 线程创建时继承父线程的优先级
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
4.线程的生命周期
线程的生命周期包含5个阶段:新建、就绪、运行、阻塞、死亡
- 新建(New): 当一个Thread类或其子类的对象被创建时,新生线程对象处于新建状态
- 就绪(Runnable):处于新建状态的线程调用start( )方法后,将进入线程队列等待CPU时间片。此时它已具备了运行的条件,只是没分配到CPU资源
- 运行(Running):当就绪的线程被调度并获得CPU资源时,便进入运行状态。run()方法定义了线程的操作和功能
- 阻塞(blocked):在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态:比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程进入就绪状态,要再次等待CPU分配资源进入运行状态
- 死亡(terminated):线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
5.线程同步
- 共享数据:多个线程共同操作的变量
- 操作共享数据的代码,即为需要被同步的代码
- 需要被同步的代码数量一定要准确:不能包含多,也不能包含代码少
- 同步监视器( 锁 ):任何一个类的对象都可以充当锁
- 多个线程必须要共用同一把锁
5.1 同步代码块
5.1.1 语法格式
synchronized(同步监视器)
{
// 需要被同步的代码
}
5.1.2 同步代码块解决继承Thread类的线程安全问题
class Bank extends Thread
{
private static double money=2000; //此处money声明为static 因为多个对象共用一个money
@Override
public void run()
{
while (money > 0) //当某个线程已经进入while内,其他线程在此处等待
{
synchronized (Bank.class)
// 一定是进入循环后上锁,而不能上锁后再进入循环(会导致只能是某个bank取完钱)
//因为当前线程上锁后进入循环后其他线程都不能进入循环,直到当前线程退出循环,也就是取完了所有的钱
{
try
{
sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(getName() + ":" + 100);
money -= 100;
}
}
}
}
public static void main(String[] args)
{
Bank bank1=new Bank();
Bank bank2=new Bank();
Bank bank3=new Bank();
bank1.start();
bank2.start();
bank3.start();
}
//输出 :bank1、bank2、bank3均有取到钱
- 共享数据声明为static (因为需要创建多个对象)
- 同步监视器使用当前类 当前类名.class
- 同步监视器不能使用 this ( 因为this表示的是当前对象,但是继承的方式会创建多个对象,违背了操作同一数据的多线程必须是同一把锁)
5.1.3 同步代码块解决实现Runnable接口的线程安全问题
class Bank1 implements Runnable
{
private double money=2000;
@Override
public void run()
{
while(money>0)
{
synchronized (this)
{
try
{
Thread.sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + 100);
money -= 100;
}
}
}
}
public static void main(String[] args)
{
Bank1 bank1=new Bank1();
Thread t1=new Thread(bank1);
Thread t2=new Thread(bank1);
Thread t3=new Thread(bank1);
t1.start();
t2.start();
t3.start();
}
//输出 :bank1、bank2、bank3均有取到钱
- 同步监视器使用this (只创建了一个Bank1类的对象,然后将这个对象传递给Thread构造器,则多个Thread类的对象共用的是一个对象,所以可以使用this,共享数据也不用声明为static )
5.2 同步方法
5.2.1 语法格式
权限修饰符 synchronized 返回值 方法名(形参)
{
// 被同步的代码块
}
5.2.2 同步方法解决继承Thread类的线程安全问题
class Bank2 extends Thread
{
private static double money=2000; //此处money声明为static 因为多个对象共用一个money
//此处的同步监视器是:Bank2.class
public synchronized void getMoney() // 将共享的数据操作代码封装为synchronized方法
{
try
{
sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(getName() + ":" + 100);
money -= 100;
}
public void run()
{
while (money > 0) //当某个线程已经进入while内,其他线程在此处等待
{
getMoney();
}
}
}
public static void main(String[] args)
{
Bank2 bank1=new Bank2();
Bank2 bank2=new Bank2();
Bank2 bank3=new Bank2();
bank1.start();
bank2.start();
bank3.start();
}
//输出 :bank1、bank2、bank3均有取到钱
5.2.3 同步方法解决实现Runnable接口的线程安全问题
class Bank3 implements Runnable
{
private double money = 2000;
//此处的同步监视器是:this
public synchronized void getMoney() // 将共享的数据操作代码封装为synchronized方法
{
try
{
Thread.sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + 100);
money -= 100;
}
public void run()
{
while (money > 0) // 当某个线程已经进入while内,其他线程在此处等待
{
getMoney();
}
}
}
public static void main(String[] args)
{
Bank3 bank3=new Bank3();
Thread t1=new Thread(bank3);
Thread t2=new Thread(bank3);
Thread t3=new Thread(bank3);
t1.start();
t2.start();
t3.start();
}
//输出 :bank1、bank2、bank3均有取到钱
5.3 同步代码块/方法中释放/不释放锁
(1) 释放锁的操作
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块,该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
(2)不会释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁 ( 应尽量避免使用suspend()和resume()来控制线程 )
5.4 Lock锁 (JDK5.0新增)
5.4.1 语法格式
private ReentrantLock lock = new ReenTrantLock();
方法A
{
lock.lock(); //上锁
......
lock.unlock();//解锁
}
注意:如果同步代码有异常,要将unlock()写入finally语句块
5.4.2 代码示例
class Lock implements Runnable
{
private double money =2000;
private ReentrantLock lock=new ReentrantLock(true);//表示线程先来后到
public void run()
{
while(money>0)
{
lock.lock();//手动上锁
try
{
Thread.sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+100);
money-=100;
lock.unlock();//手动解锁
}
}
}
public static void main(String[] args)
{
Lock mylock=new Lock();
Thread t1=new Thread(mylock);
Thread t2=new Thread(mylock);
Thread t3=new Thread(mylock);
t1.start();
t2.start();
t3.start();
}
//输出 :bank1、bank2、bank3均有取到钱
5.5 Synchronized和Lock的区别
6 线程通信
6.1 涉及到的三个方法
- wait( ) :一旦执行此方法,当前线程就进入阻塞状态,并释放锁
- notify( ):一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait则唤醒优先级高的那个。如果优先级相同则随机唤醒
- notifyAll( ):一旦执行此方法,就会唤醒所有被wait的线程。
①wait( ),notify( ),notifyAll( ) 三个方法必须使用在同步代码块或同步方法中
②wait( ),notify( ),notifyAll( )三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
③ wait( ),notify( ),notifyAll( )三个方法是定义在java.lang.Object类中
6.2 sleep() 和 wait( )的异同
- 相同点:
-
一旦执行方法,都可以使得当前的线程进入阻塞状态
- 不同点:
-
两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
-
调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
-
关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁
6.3 代码示例
双线程交替打印 1-20
class Number implements Runnable
{
private int num = 1;
public void run()
{
while (num <= 20)
{
synchronized (this)
{
notifyAll();//唤醒所有线程
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
try
{
wait(); //此时阻塞且释放锁
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
}
// 线程A进入while循环后上锁,打印后num++,然后执行wait()后线程A阻塞且释放锁。
// 线程B进入while循环后上锁,然后唤醒线程A(进入等待转态),线程B打印后num++,然后执行wait()后线程B阻塞且释放锁
// 重复....
public static void main(String[] args)
{
Number number=new Number();
Thread t1=new Thread(number);
Thread t2=new Thread(number);
t1.start();
t2.start();
}