Java高级--多线程

多线程

什么是多线程

什么进程?什么是线程?

进程是一个应用程序.线程是一个进程中的执行场景/执行单元.

一个进程可以启动多个线程.

对于java程序来说,当dos命令窗口中输入:java HelloWorld回车之后

会先启动JVM,而JVM就是一个进程.

同时再启动一个垃圾回收线程负责看护,回收垃圾.

一个是垃圾回收线程,一个是执行main方法的主线程

进程可以看做是公司 而线程就是公司中的某个员工

注意:进程A和进程B的内存独立不共享

在Java语言中线程A和线程B,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈

假设启动10个线程,会有10个栈空间,每个栈和每个栈之间互不打扰,这就是多进程并发.

实现多线程的方式

Java语言中实现线程有两种方式:

第一种方式:编写一个类,直接继承java.lang.Thread重写run方法

public class MyThread extends Thread{
    public void run(){
        
    }
}
//创建线程对象
MyThread t = new MyThread();
//启动线程
t.start();
public class ThreadTest01 {
    public static void main(String[] args) {
        //这里是main方法,这里的代码属于主线程,在主栈中运行.
        //这里新建一个分支线程对象
        MyThread mythread = new MyThread();
        //启动线程
        //start()方法的作用:启动一个分支线程,在JVM中开辟一个新的空间,这段代码任务完成后瞬间就结束了.
        //这段代码的任务只是为了开启一个新的栈空间,只要新的空间开出来,start方法就结束了.线程就启动成功了
        //启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈).
        //run方法在分支栈的栈底部,main方法在主栈的栈底部.run和main是平级的.
        mythread.start();
        for (int i = 0;i<=100;i++ ){
            System.out.println("主线程-->"+i);
        }
    }
}
class MyThread extends Thread{
    @Override
    public void run(){
        //编写程序,这段程序运行在分支线程中(分支线)
        for(int i = 0;i<=100;i++){
            System.out.println("分支线程-->"+i);
        }
    }
}

第二种方式:

编写一个类,实现java.lang.Runnable接口,实现run方法

//定义一个可运行的类
public class MyRunnable implements Runnable {
    public void run(){    
    }
}
//创建线程对象
Thread t = new Thread (new MyRunnable());
//启动线程
t.start();
public class ThreadTest03 {
    public static void main(String[] args) {

        //实现多线程的第二种方式,编写一个类实现java.lang.Runnable接口
        //创建一个可运行的对象
        MyRunnable r = new MyRunnable();
        //将可运行的对象封装成一个线程对象
        Thread t = new Thread(r);
        //Thread t = new Thread(new MyRunnable());//合并代码
        //启动线程
        t.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程-->" + i);
        }
    }
}
    //这不是一个线程类 是一个可运行的类 他还不是一个线程
    class MyRunnable implements Runnable {
    @Override
        public void run(){
        for (int i = 0 ; i<100;i++){
            System.out.println("分支线路-->"+i);
        }
    }
}

注意:第二种方式实现接口比较常用,因为第一个类实现了接口,它还可以去继承其他的类,更灵活

线程的生命周期

image.png

线程的状态

New 新创建

Runable 可运行 ,等待jvm调度

Blocked 被阻塞

Waiting 等待

Timed waiting 计时等待

Terminated 被终止

获取状态 : getState 方法

image.png

Thread

void join()

等待终止指定线程

void join(long millis)

等待指定的线程死亡或者经过指定的毫秒数

Thread.State getState()

示例:通过join可以等待另一个线程直到结束

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(()->{System.out.println("hello");});
    System.out.println("start");
    t.start();
    t.join();
    System.out.println("end");
    }

线程中断

Thread

void interrupt()

向线程发送中断请求,线程的中断状态设置为true,如果目前被sleep调用阻塞,那么

InterruptedException异常抛出

static boolean interrupted()

测试当前线程是否被中断,并中断状态重置为false

static boolean isInterrupted()

测试当前线程是否被中断

线程中断状态:

//方式一,通过interrupt 
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1);// 暂停1毫秒
        t.interrupt(); // 中断t线程
        t.join(); // 等待t线程结束
        System.out.println("end");
        } 
}
class MyThread extends Thread {
    public void run() {
        int n = 0; 
        while (! isInterrupted()) { 
            n ++; 
            System.out.println(n + " hello!");
        }
    }
}
//方式二:通过volatiles变量
public class Main { public static void main(String[] args) throws InterruptedException { 
    HelloThread t = new HelloThread();
    t.start();
    Thread.sleep(1);
    t.running = false; // 标志位置为false 
        }
}
class HelloThread extends Thread {
    public volatile boolean running = true;
    public void run() {
        int n = 0; 
        while (running) {
            n ++;
            System.out.println(n + " hello!");
            }
        System.out.println("end!");
    }
}

线程同步

同步编程模型:

    线程t1和线程t2,在线程执行的时候,必须等待t2线程执行结束.两个线程之间发生了等待关系,这就是同步编程模型

    效率较低,线程排队执行  

异步编程模型:

    线程t1和线程t2各自执行各自的,谁都不需要等谁,这种编程模型叫异步编程模型.其实就是多线程并发(效率较高)

不使用线程同步机制:

//银行账户
public class Account {
    //账号
    private String actno;
    //余额
    private double balance;

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款方法
    public void withdraw(double money){
        //t1和t2并发这个方法
        //取款之前的余额
        double before =this.getBalance();
        //取款之后的余额
        double after = before - money;
        //在这里模拟延迟,100%会出问题
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //更新余额
        //t1执行到这里了,但还没来得及执行这行代码,t2线程进来withdraw方法,此时一定出问题
        this.setBalance(after);
    }

}
public class AccountThread extends Thread{

    //两个线程必须共享同一个账户对象
    private  Account act;
    public  AccountThread(Account act){
        this.act = act;
    }

    public void run(){
         //run方法的执行表示取款操作
        //假设取款5000
        double money = 5000;
        //取款
        act.withdraw(money);
        System.out.println("对账户"+act.getActno()+"取款成功.账户余额"+act.getBalance());
    }
}
public class Test {
    public static void main(String[] args) {
        //创建账户对象(只创建一个)
        Account act = new Account("act-001",10000);
        //创建两个线程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);
        //设置name
        t1.setName("t1");
        t2.setName("t2");
        //启动线程取款
        t1.start();
        t2.start();
    }
}
t2对act-001取款成功.账户余额5000.0
t1对act-001取款成功.账户余额5000.0
//出现严重错误

使用线程同步机制--synchronized:

package threadsafe2;
//银行账户
//使用线程同步机制
public class Account {
    //账号
    private String actno;
    //余额
    private double balance;

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款方法
    public void withdraw(double money) {
        //以下这几行代码必须是线程排队的,不能并发
        //一个县城把这里的代码全部执行后,另一个线程才能进来
        /*
        线程同步机制的语法是:
            synchronized(){
            //线程同步代码块
            }
            synchronized后面括号中传递的数据是相当关键的
            这个数据必须是多线程共享的数据,才能达到多线程排队
            ()中写什么要看想让哪些线程同步
            假设t1,t2,t3,t4,t5有五个线程
            只希望t1,t2,t3排队,t4,t5不需要排队,怎么办?
            一定要在()中写t1,t2,t3共享的对象.而这个对象对于t4,t5来说不是共享的

            这里的共享对象是账户对象
            账户对象是共享的,那么this就是账户对象
            不一定是this,这里只要是多线程共享的对象就行
            在java语言中 任何一个对象 都是有"一把锁",其实这把锁就是一个标记(只是把他叫做锁)
            100个对象100把锁 1个对象1把锁

            以下代码的执行原理是:
                1.假设t1 t2线程并发开始执行以下代码的时候肯定有一个先一个后
                2.假设t1先执行了,遇到了synchronized,这个时候自动找"后面共享对象的对象锁"
                找到之后并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的
                直到同步代码块代码结束,这把锁才会释放.
                3.假设t1已经占有了这把锁,此时t2也遇到了synchronized关键字,也会去占有后面
                共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束
                直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁后
                进入同步代码块执行程序.

                这样就达到了线程排队执行.
                这里需要注意的是这个共享对象一定要选好.这个共享对象一定是你需要排队执行的这些线程对象所共享的
         */
        synchronized (this) {
            double before = this.getBalance();
            double after = before - money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(after);
        }
    }
}

锁池:

image.png

Java中有三大变量:

实例变量:在堆中

局部变量:在栈中

静态变量:在方法区中

以上三大变量中:

    局部变量永远都不会存在线程安全问题.

    因为局部变量不共享(一个线程一个栈).

实例变量在堆中,堆只有一个,静态变量在方法区中,方法区只有一个.

堆和方法区都是多线程共享的,所以可能存在线程安全问题.

局部变量+常量:不会有线程安全问题

成员变量:可能会有安全问题

//取款方法
/*
在实例方法上可以使用synchronized吗?----可以
    synchronized出现在实例方法上,一定锁的是this
    只能是this,不能是其他的对象了.
    所以这种方式不灵活
    另外还有一个缺点:
    synchronized出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围
    导致程序的执行效率降低.所以这种方式不常用

    synchronized使用在实例方法上有什么优点?
    代码量少.简洁.

    如果共享的对象就是this   并且需要同步的代码块就是整个方法体  建议使用这种方式
 */
public synchronized void withdraw(double money) {
        double before = this.getBalance();
        double after = before - money;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setBalance(after);
    }
}

如果使用局部变量的话:

建议使用:StringBuilder

因为局部变量不存在线程安全问题.选择StringBuilder.

StringBuffer效率比较低.

Arrylist是非线程安全的

Vector是线程安全的

HashMap HashSet是非线程安全的

HashTable是线程安全的

总结

synchronized有三种写法

第一种:同步代码块

    灵活

    synchronized(线程共享对象){

    同步代码块

    }

第二种:在实例方法上使用synchronized

    表示共享对象一定是this

    并且同步代码块是整个方法体

第三种:在静态方法上使用synchronized

        表示找类锁.

        类锁永远只有一把

        就算创建了100个对象,类锁也只有一把

    对象锁:1个对象1把锁 100个对象100把锁

    类锁:100个对象也可能只有1把锁

synchronized在开发中最好不要嵌套使用,一不小心就会导致死锁现象

线程池

FixedThreadPool:线程数固定的线程池;

CachedThreadPool:线程数根据任务动态调整的线程池;

SingleThreadExecutor:仅单线程执行的线程池

scheduledThreadPool 定期执行

守护线程

java语言中线程分为两大类:

一类是用户线程     一类是守护线程(后台线程)

其中最具有代表性的是:垃圾回收线程.

守护线程的特点:

一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束.

注意:主线程main方法是一个用户线程

package threadTest;

public class ThreadTest04 {
    public static void main(String[] args) {
        Thread t = new BakDataThread();
        t.setName("备份数据的线程");
        //在启动之前,将线程设置为守护线程
        t.setDaemon(true);
        t.start();

        //主线程:主线程是用户线程
        for(int i = 0; i<10;i++){
            System.out.println(Thread.currentThread().getName()+"-->"+ i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class BakDataThread extends  Thread{
    public void run(){
        int i = 0;
        //即使是死循环,但由于该线程是守护线程,当用户线程结束,守护线程自动终止
        while(true){
            System.out.println(Thread.currentThread().getName()+"-->"+(++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

定时器

定时器的作用:间隔特定的时间执行特定的程序.

每周要进行银行账户的总账操作.

每天要进行数据的备份操作.

在实际的开发中,每隔多久执行特定的一段程序,这种需求是很常见的.

在Java中可以采用多种方式实现:

可以使用sleep方法,设置睡眠时间,每到这个时间点醒来执行任务.

这种方式是最原始的定时器.

在Java类库中已经写好了一个定时器:java.util.timer,可以直接拿来用

这种方式在目前的开发中也很少用.因为现在有很多高级框架都是支持定时任务的

在实际开发中目前使用比较多的是Spring框架中的SpringTask框架.

只需要简单的配置就可以完成定时器的任务.
/*
使用定时器指定定时任务
 */
public class TimerTest {
    public static void main(String[] args) throws ParseException {
        //创建定时器对象
        Timer timer = new Timer();
        //Timer timer = new Timer(true);       守护线程的方式

        //指定定时任务
//        timer.schedule(定时任务,第一次执行时间,间隔多久);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date firstTime=sdf.parse("2021-07-28 15:17:01");
        timer.schedule(new LogTimerTask(),firstTime,1000);
    }
}

//编写一个定时任务类
//假设这是一个记录日志的定时任务
class LogTimerTask extends TimerTask {

    @Override
    public void run() {
        //编写需要执行的任务
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String strTime = sdf.format(new Date());
        System.out.println(strTime+":成功完成了一次数据备份");
    }
}

Callable接口

这种方式实现的线程可以获取线程的返回值.

之前讲解的那两种方式是无法获取线程的返回值的,因为run方法返回void.

系统委派一个线程去执行一个任务,该线程执行完任务之后可能会有一个执行结果,我们怎么能拿到这个执行结果呢,使用第三种方式--实现callable接口
package threadTest;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask; //JUC包下的,属于Java并发包 老JDK中没有这个包.新特性

/*
实现线程的第三种方式:
    实现Callable接口
 */
public class ThreadTest05 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //第一步需要创建一个 未来任务类 对象
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {     //call方法相当于run方法.只不过这个有返回值
                //线程执行一个任务 执行之后可能会有一个返回结果
                //模拟执行
                System.out.println("call method begin");
                Thread.sleep(1000 * 5);
                System.out.println("call method end");
                int a = 100;
                int b = 200;
                return a + b; //自动装箱 (300结果变成Integar)
            }
        });
    //创建线程对象
        Thread t = new Thread(task);
        //启动线程
        t.start();

        //这里是main方法,这是主线程中
        //在主线程中怎么获取t线程的返回结果?
        //get()方法的执行会导致"当前线程的阻塞"
        Object obj = task.get();
        System.out.println("线程执行结果:"+obj);
        //main方法这里的成宿要想执行必须等待get()方法的结束
        //而get()方法可能需要很久.因为get方法是为了拿另一个线程的执行结果
        //而另一个线程执行是需要时间的
        System.out.println("hello world!");

    }
}

Future保存异步计算结果,将Future交给某个线程,然后忘掉它,计算后可以获得它

public interface Future<V>{
    V get() throws ..
    V get(long timeout,TimeUnit unit) throws ..;
    void cancel(boolean myInterrupt)
    boolean isCancelled();
    boolean isDone();
    }

FutureTask是一种很便利的机制,可以将Callable转为Future和Runable,实现了二者的接口

Callable<Integer> muCom = .. 
FutureTask<Integer> task =t new FutureTask<Integer>(myCom) 
Thread t = new Thread(task)
t.start()

submit 返回future

ExecutorService executor = Executors.newFixedThreadPool(4); // 定义任务:
Callable<String> task = new Task(); 
// 提交任务并获得
Future: Future<String> future = executor.submit(task);
// 从Future获取异步执行返回的结果: 
String result = future.get();
// 可能阻塞

Callable

V call()

Future

V get()

V get(long time,TimeUnit unit)

boolean cancel()

boolean isDone()

FutureTask(Callable task)

FutureTask(Runnable task,V result)

ThreadLocal的使用

对于多任务,Java标准库提供的线程池可以方便地执行这些任务,同时复用线程。Web应用程序就是典

型的多任务应用,每个用户请求页面时,我们都会创建一个任务,类似:

public void process(User user) {
    checkPermission(user);
    doWork(user); 
    saveStatus(user); 
    sendResponse(user); 
}

其中每一个方法可能会调用多个方法,使用起来非常不方便,所以java提供了ThreadLocal

保存一个线程的共享数据

static ThreadLocal<User> threadLocalUser = new ThreadLocal<>(); 
void processUser(user) {
    try {
        threadLocalUser.set(user);
        step1(); 
        step2();
    } finally { t
        hreadLocalUser.remove(); 
               //一定要清除,因为使用了线程池,线程会重复使用 
     }
    }//使用 
void step1() {
    User u = threadLocalUser.get(); 
    log(); 
    printUser(); 
}
void log() {
    User u = threadLocalUser.get();
    println(u.name);
}
void step2() { 
    User u = threadLocalUser.get(); 
    checkUser(u.id); 
}

实际上,可以把 ThreadLocal 看成一个全局 Map<Thread, Object> :每个线程获取 ThreadLocal 变

量时,总是使用 Thread 自身作为key:

Object threadLocalValue = threadLocalMap.get(Thread.currentThread());

为了保证一定被关闭,可以封装一次,实现AutoCloseable ,调用时使用带资源的try,实际不建议采用

这种方式,因为调用代码和关闭并不在一起

public class UserContext implements AutoCloseable {
    static final ThreadLocal<String> ctx = new ThreadLocal<>();
    public UserContext(String user) {
        ctx.set(user);
    }
    public static String currentUser() {
        return ctx.get(); 
    }
    @Override public void close() {
        ctx.remove();
    } 
}
//调用
try (var ctx = new UserContext("Bob")) {
    // 可任意调用UserContext.currentUser():
    String currentUser = UserContext.currentUser(); 
} // 在此自动调用UserContext.close()方法释放ThreadLocal关联对象
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容