多线程的生命周期:
- 新建态:创建一个线程对象。
- 就绪态:线程对象创建完成之后,加入到可运行的线程池中,等待系统调用,获取cpu的使用权。
- 运行态:获取了cpu的使用权,执行代码。
- 阻塞态:在运行的过程中,由于某些原因放弃了cpu的使用权,停止运行,直到回到就绪态。
- 死亡态:线程执行完毕,或因为异常结束运行,即退出run()。
多线程的四种创建方式
-
继承Thread类
创建一个类继承自Thread,重写run方法。
public class Thread1 extends Thread implements StartThread {//StartThread接口提供了一个启动线程的抽象方法
@Override
public void run() {//重写run方法
for (int i = 0; i < 100; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
@Override
public void startThread() {
Thread1 th = new Thread1();//创建当前类的实例
th.start();//启动线程调用Thread类的start()方法
}
}
创建测试类,查看运行结果
public class ThreadTest{
public static void main(String[] args) {
StartThread ct1 = new Thread1();//实例化一个Thread1对象
ct1.startThread();//启动线程
}
}
-
实现Runnable接口
创建一个类实现Runnable接口,重写run()方法
public class RunnableTest1 implements Runnable, StartThread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
@Override
public void StartThread() {
/**
* 创建一个Thread类的实例。
* 参数是实现了Runnable接口中的run()方法的一个实例对象
* 启动线程调用start()方法
*/
Thread th = new Thread(new RunnableTest1());
th.start();
}
}
创建测试类,查看运行效果
public class ThreadTest{
public static void main(String[] args) {
StartThread ct1 = new Thread1();
ct1.startThread();
}
}
-
实现Callable接口,与FutureTask类结合使用
创建StartThread接口
public interface StartThread {
public void startThread() throws ExecutionException, InterruptedException;//开启线程
public void getResult();//获取线程执行完毕返回的结果
public boolean aliveThread();//判断当前线程是否存活
}
创建CallableTest1类,实现Callable、StartThread接口
public class CallableTest1 implements Callable<List<String>> ,StartThread{
FutureTask<List<String>> ft = null;//Callable接口创建线程需要与FutureTask类一起使用
Thread th = null;
List<String> list = null;//储存线程执行结果
@Override
public List<String> call() throws Exception {
List<String> list = new ArrayList<>();
String str = null;
Date date = new Date();
for (int i = 0; i < 100; i++) {
if(i % 2 != 0){
str = "运行时间:" + date.getTime() + "----" + Thread.currentThread().getName() + "--->" + i;
System.out.println("运行中..." + str);
list.add(str);
}
}
return list;
}
@Override
public void startThread() throws ExecutionException, InterruptedException {
ft = new FutureTask<>(new CallableTest1());
th = new Thread(ft);
th.start();
}
public void getResult() {
try {
list = ft.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
for (String str : list) {
System.out.println("结果..." + str);
}
}
@Override
public boolean aliveThread() {
return th.isAlive();
}
}
创建Test测试类,查看运行结果
public class Test {
public static void main(String[] args) {
StartThread st1 = new CallableTest1();
try {
st1.startThread();
while(st1.aliveThread()){
}
st1.getResult();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
注意:
我在这里犯过几个错误,在这里为大家展示一下希望大家能够避免少走弯路。
抽象出的方法功能要尽量单一,比如这里的StartThread()方法
@Override
public void startThread() throws ExecutionException, InterruptedException {
/**
* 启动线程方法只负责启动线程
*/
ft = new FutureTask<>(new CallableTest1());
th = new Thread(ft);
th.start();
/**
* 如果在启动线程之后还有耗时的操作发生,那么在启动其他线程时就会有问题
* 例如,我在这里再实现一个对于执行结果的接收和遍历。
* 那么当我在测试类中调用该方法时,线程启动后还需要执行完剩余的代码才能够退出该方法
* 如果同时要启动多个Callable接口实现类的对象启动线程,其执行效果就是单线程的效果
*/
// list = ft.get();
// for (String str : list) {
// System.out.println(str);
// }
}
在实现类中,FutureTask类设计为全局变量时,尽量只声明该类的引用,不要直接创建实例对象。(会造成栈空间溢出的异常,这一点不仅在本实验中会发生,所以希望大家能在实现类的时候也注意这一点)
public class CallableTest1 implements Callable<List<String>> ,StartThread{
// FutureTask<List<String>> ft = new FutureTask<>(new CallableTest1());
FutureTask<List<String>> ft = null;//Callable接口创建线程需要与FutureTask类一起使用
-
使用线程池创建线程
使用线程池的好处:
- 线程在创建线程池的时候就创建好,在使用线程池时直接提供创建好的线程,节省了创建线程的时间,提高了效率。
- 线程在使用完毕后不会直接销毁,而是放回到线程池中,这样就不需要每次都去创建一个线程,减少了资源的消耗。
- 可以设置线程池的大小、最大线程数、线程多久没有任务以后会关闭等等,方便对线程进行管理
package Demo3;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.lang.Thread.sleep;
public class ThreadDemo implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i % 2 == 0 ? Thread.currentThread().getName() + "----" + i : "");
}
}
}
class ThreadPool{
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);//提供线程数量的线程池
/**
* 有两种参数:
* 一.实现Runnable接口的实现类
* 二.实现Callable接口的实现类
*/
service.submit(new ThreadDemo());
}
}
多线程中的方法:
-
join():
调用join方法,当前线程进入阻塞状态等待加入的线程执行完毕,再继续执行。
import static java.lang.Thread.sleep;
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
MyThread mt = new MyThread();
Thread th = new Thread(mt);
th.start();
th.join();//将子线程加入到main线程中,此时main线程等待子线程执行完毕,再恢复执行
for(int i = 0; i < 20; i ++){
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "====" + i);
}
}
}
class MyThread implements Runnable{
@Override
public void run() {
for(int i = 0; i < 20; i ++){
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "====" + i);
}
}
}
-
setPriority(int newPriority):
设置线程的优先级,JDK一共提供了3种线程执行的级别,默认优先级是5
Thread th = new Thread();
th.setPriority(MAX_PRIORITY);//设置th线程为最高优先级——>10
th.setPriority(MIN_PRIORITY);//设置th线程为最低优先级——>1
th.setPriority(NORM_PRIORITY);//设置th线程为默认优先级——>5
注意:设置了线程的优先级只是使得该线程被给予资源的概率提高,并不能够保证该线程一定会被优先执行,说到底还是看脸,概率是一个玄学。。。
-
sleep(long millis):
强迫当前线程进入阻塞状态暂停执行millis毫秒,但是不会丢失任何监视器的所属权。当暂停时间到达之后,该线程回到就绪状态继续执行。
-
yield():
当前线程调用了yield后,该线程不会变成阻塞状态,而是直接回到就绪状态。
-
Sleep(long millis)与yield()的区别
-
线程安全问题
多个线程操作同一个共享资源的时候就会产生线程安全的问题,比如经典的买票事件。
多个卖票窗口(线程)售卖同一类门票(共享资源),如果在线程不安全的情况下就可能会发生重票的现象。
package Demo1;
import static java.lang.Thread.sleep;
public class Test {
public static void main(String[] args) {
/**
* 通过结果显示,多个窗口会多次售出同一张票,这就是线程安全的问题
*/
Thread1 t = new Thread1();
Thread th1 = new Thread(t, "窗口一");
Thread th2 = new Thread(t, "窗口二");
th1.start();
th2.start();
}
}
class Thread1 implements Runnable{
int ticket = 20;//多个线程共享的资源
@Override
public void run() {
while (ticket > 0){
try {
sleep(100);//当前线程在这里睡眠一会,其他线程进入循环可增加重票概率
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "剩余" + ticket + "张票");
}
}
}
-
线程锁
为了解决线程安全的问题,Java提供了线程锁的方法,阻塞线程,保证在锁空间内单线程执行。
-
synchronized()同步锁:
使用synchronized锁有两种方式,一种是同步代码块,另外一种是同步方法。对于加锁后的区域,可以保证同一时刻只有一个线程在执行,其他线程在线程池中等待该线程释放锁,有效的解决了线程安全的问题。但缺点是,由于加锁后只能单线程执行,所以效率会变差。
同步代码块:在指定的区域内加锁,在使用同步代码块进行加锁时,需要为同步代码块提供锁(也是监视器),要保证需要同步的线程使用同一把锁才能够实现线程安全,监视器可以为任意一对象,同步力度(可以理解为作用区域)小于同步方法。
synchronized(Object obj){
//执行代码
}
同步方法:在执行方法的返回值前添加synchronized,不用指定监视器,因为监视器指定为当前调用该方法的对象,即为this,同步方法同步力度大于同步代码块。
//synchronized放在返回类型前任意位置即可
public synchronized void method1(){
//执行代码
}
synchronized public void method2(){
//执行代码
}
对之前的卖票流程进行修改,实现线程的安全。
package Demo1;
import static java.lang.Thread.sleep;
public class Test {
public static void main(String[] args) {
/**
* 通过结果显示,先获得锁的窗口直到将票卖完,剩下的窗口也不能够卖票
* 这是因为先得到锁的窗口一直拿着锁将票卖完之后才释放,剩下的窗口自然无票可卖
*/
Thread1 t = new Thread1();
Thread th1 = new Thread(t, "窗口一");
Thread th2 = new Thread(t, "窗口二");
th1.start();
th2.start();
}
}
class Thread1 implements Runnable{
int ticket = 100;//多个线程共享的资源
@Override
public void run() {
try {
sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Thread.class){
while (ticket > 0){
try {
sleep(100);//当前线程在这里睡眠一会,其他线程进入循环可增加重票概率
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "剩余" + ticket + "张票");
}
}
}
}
-
Lock接口:
Lock接口是在JDK1.5之后提出的一种锁,使用Lock接口锁的实现类,我们同样可以达到线程安全的效果。在与synchronized效果一致的同时,Lock更加灵活(synchroized是自动加锁、自动释放,Lock是手动加锁、手动释放)。