JavaSE--多线程

1.进程和线程

1.1、定义

进程:

是一个应用程序

线程:

是一个进程中的执行场景/执行单元。

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

例:对于java程序来说,当在DOS命令窗口中输入:
java HelloWorld 回车之后。
会先启动JVM,而JVM就是一个进程。
JVM再启动一个主线程调用main方法。
同时再启动一个垃圾回收线程负责看护,回收垃圾。
最起码,现在的java程序中至少有两个线程并发,
一个是垃圾回收线程,一个是执行main方法的主线程。

1.2、进程和线程关系

例子:

​ 阿里巴巴:进程
​ 马云:阿里巴巴的一个线程
​ 童文红:阿里巴巴的一个线程

​ 京东:进程
​ 强东:京东的一个线程
​ 妹妹:京东的一个线程

​ 进程可以看做是现实生活当中的公司。
​ 线程可以看做是公司当中的某个员工。

注意:
进程A和进程B的内存独立不共享。(阿里巴巴和京东资源不会共享的!)
魔兽游戏是一个进程
酷狗音乐是一个进程
这两个进程是独立的,不共享资源。

​ 线程A和线程B呢?
​ 在java语言中:
​ 线程A和线程B,堆内存和方法区内存共享。
​ 但是栈内存独立,一个线程一个栈。

​ 假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,
​ 互不干扰,各自执行各自的,这就是多线程并发。

​ 火车站,可以看做是一个进程。
​ 火车站中的每一个售票窗口可以看做是一个线程。
​ 我在窗口1购票,你可以在窗口2购票,你不需要等我,我也不需要等你。
​ 所以多线程并发可以提高效率。

​ java中之所以有多线程机制,目的就是为了提高程序的处理效率。

1.3、问题1:多线程机制中,main方法结束,程序会结束吗

​ 使用了多线程机制之后,main方法结束,是不是有可能程序也不会结束。
​ main方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在
​ 压栈弹栈。

​ 一个线程一个栈

1.4、问题2:对于单核的CPU来说,真的可以做到真正的多线程并发吗?

​ 对于多核的CPU电脑来说,真正的多线程并发是没问题的。
​ 4核CPU表示同一个时间点上,可以真正的有4个进程并发执行。

​ 什么是真正的多线程并发?
​ t1线程执行t1的。
​ t2线程执行t2的。
​ t1不会影响t2,t2也不会影响t1。这叫做真正的多线程并发。

​ 单核的CPU表示只有一个大脑:
​ 不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。
​ 对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于
​ CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是:多个事情
​ 同时在做!!!!!
​ 线程A:播放音乐
​ 线程B:运行魔兽游戏
​ 线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在运行,
​ 给我们的感觉是同时并发的。

​ 电影院采用胶卷播放电影,一个胶卷一个胶卷播放速度达到一定程度之后,
​ 人类的眼睛产生了错觉,感觉是动画的。这说明人类的反应速度很慢,就像
​ 一根钢针扎到手上,到最终感觉到疼,这个过程是需要“很长的”时间的,在
​ 这个期间计算机可以进行亿万次的循环。所以计算机的执行速度很快。

1.5、线程start方法和run方法的区别

1.start方法

start()方法的作用是:

启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码在任务完成之后,瞬间就结束了。

start方法的运行原理

2.run方法

2.实现线程的两种方法

java支持多线程机制。并且java已经将多线程实现了,我们只需要继承就行了。

2.1.第一种方式:

编写一个类,直接继承java.lang.Thread,重写run方法。

// 定义线程类
		public class MyThread extends Thread{
			public void run(){
			
			}
		}
		// 创建线程对象
		MyThread t = new MyThread();
		// 启动线程。
		t.start();
	

例:

package com.javase.Hashtable.Thread;

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


class MyThread extends Thread {
    @Override
    public void run() {
        //编写程序,这段程序运行在分支线程中(分支栈)
        for (int i = 0; i < 1000; i++) {
            System.out.println("分支线程------" + i);
        }
    }
}

2.2.第二种方式

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

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

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

例:

package com.javase.Hashtable.Thread;

public class ThreadTest02 {
    public static void main(String[] args) {
/*        //创建一个可运行对象
        MyRunnalble r = new MyRunnalble();
        //将可运行的对象封装成一个线程对象
        Thread t = new Thread(r);*/
        //上述语句可以缩写成
        Thread t = new Thread(new MyRunnalble());
        //启动线程
        t.start();

        for (int i = 0; i < 1000; i++){
            System.out.println("主线程" + i);
        }
    }
}

class MyRunnalble implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++){
            System.out.println("分支线程" + i);
        }
    }
}

2.3.采用匿名内部类创建线程对象

package com.javase.Hashtable.Thread;

public class ThreadTest03 {
    public static void main(String[] args) {
        //采用匿名内部类创建线程对象
        Thread t = new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++){
                    System.out.println("分支线程" + i);
                }
            }
        });
        //启动线程
        t.start();

        for (int i = 0; i < 100; i++){
            System.out.println("主线程" + i);
        }
    }
}

3.线程的声明周期

线程的生命周期存在五个状态:新建、就绪、运行、阻塞、死亡

4.线程调度与控制

4.1.修改线程名字

void setName(String name) 
//改变线程名称,使之与参数 name 相同。 

4.2.获取线程名字

String getName() 
//返回该线程的名称。 

4.3.获取当前线程对象

static Thread currentThread() 
// 返回对当前正在执行的线程对象的引用。 

这个代码在哪获得的就是哪里的线程

Thread t = Thread.currentThread();

返回值t 就是当前线程

4.4.sleep方法

static void sleep(long millis) 
//让当前线程进入休眠,进入"阻塞状态",放弃占有CPU时间片,让其他线程使用 

package com.javase.Hashtable.Thread;

public class ThreadTest04 {
    public static void main(String[] args) {
        try {
            System.out.println("甘雨我老婆");
            Thread.sleep(1000 * 5);
            System.out.println("我说的");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4.5.终止线程的睡眠

void interrupt() 
//中断线程的睡眠。
//这种中断睡眠的方式依靠了java的异常处理机制
//运行时会在slepp方法报异常,然后程序继续运行

4.6.终止线程

void stop()   
/*这个方法已经过时了。
这种方式缺点:容易丢失数据,因为这种方式是直接将线程杀死。
线程没有保存的数据将会丢失,不建议使用*/

推荐使用 布尔标记方法

package com.javase.Hashtable.Thread;

public class ThreadTest05 {
    public static void main(String[] args) {
        MyThread02 r = new MyThread02();
        Thread t1 = new Thread(r);
        t1.getName();
        t1.start();

        for (int j = 0; j < 10; j++){
            System.out.println(Thread.currentThread().getName() + "-------->" + j);
        }

        //睡眠五秒
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //终止进程
        //想要什么时候终止进程,就什么时候把标记改成false就可以了
        r.run = false;
    }
}

class MyThread02 implements Runnable {
    //打一个布尔标记
    boolean run = true;
    @Override
    public void run() {
        for (int i = 0; i < 10; i++){
            if(run) {
                System.out.println(Thread.currentThread().getName() + "---->" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
                //return就结束了,你在结束之前还有什么没保存的。
                //在这里就可以保存了。
                //终止当前线程
                return;
            }
        }
    }
}

4.7.常见的线程调度模型

抢占式调度模型:

​ 那个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。

​ java采用的就是抢占式调度模型。

均分式调度模型:

​ 平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样。

​ 平均分配,一切平等。

​ 有一些编程语言,线程调度模型采用的是这种方式。

4.7.1 java中与线程调度有关的方法

实例方法:

1.设置线程的优先级
void setPriority(int newPriority) 
//设置线程的优先级
2.获取线程优先级
int getPriority() 
//获取线程优先级
3.线程优先级

线 程 优 先 级 主 要 分 三 种 :

MAX_PRIORITY(最高级); 最高优先级10

MIN_PRIORITY(最低级); 最低优先级1

NOM_PRIORITY(标准)默认; 默认优先级是5

优先级比较高的获取CPU时间片可能会多一些。(但也不完全是,大概率是多的。)

静态方法:

1.让位方法
static void yield()  让位方法
//暂停当前正在执行的线程对象,并执行其他线程

yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。

yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。

注意:在回到就绪之后,有可能还会再次抢到。

实例方法

1.合并线程
void join()  
//合并线程
//等待该线程终止,再继续执行原本的线程

class MyThread1 extends Thread {
			public void doSome(){
				MyThread2 t = new MyThread2();
				t.join(); // 当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续。
			}
		}

class MyThread2 extends Thread{
			
}

5.线程安全问题

关于多线程并发环境下,数据的安全问题。

5.1.重要性

以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。

最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。(重要)

5.2.数据在多线程并发的环境下会存在安全问题

三个条件:
条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为。

满足以上3个条件之后,就会存在线程安全问题。

5.3.解决线程安全问题

当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?

用排队执行解决线程安全问题。这种机制被称为:线程同步机制

专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。

线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。

5.4.线程同步和线程异步

5.4.1.异步编程模型:

线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。

其实就是:多线程并发(效率较高。)

异步就是并发。

5.4.2.同步编程模型:

线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,

两个线程之间发生了等待关系,这就是同步编程模型。效率较低。线程排队执行。

同步就是排队。

5.5.线程同步机制语法(synchronized)

synchronized(){
	//线程同步代码块
} 

synchronized后面的小括号中传的这个"数据"是很关键的。这个数据必须是多线程共享的数据,才能达到多线程排队。

()中写什么?
看想让哪些线程同步。
假设t1,t2,t3,t4,t5,有五个线程,只希望t1,t2,t3线程排队,t4,t5不需要排队。
一定要在()写一个t1,t2,t3共享的对象。而这个对象对于t4 t5来说不是共享的

例:银行用户取钱

Account类

package com.javase.Hashtable.Threadsafe;

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(this){
            //取款前的余额
            double before = this.getBalance();
            //取款后的余额
            double after = before - money;
            //模拟一下网络延迟
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //更新余额
            this.setBalance(after);
        }
        }
}

AccountThread类

package com.javase.Hashtable.Threadsafe;

public class AccountThread extends Thread {
    //两个线程共享一个账户
    private Account act;

    //通过构造方法传递过来账户对象

    public AccountThread(Account act) {
        this.act = act;
    }

    @Override
    public void run() {
        //run方法的执行表示取款操作
        //假设取款5000
        double money = 5000;
        //取款
        act.withdraw(money);

        System.out.println(Thread.currentThread().getName() + "对" +act.getActno() + "取款成功,余额" + act.getBalance());
    }
}

Test测试类

package com.javase.Hashtable.Threadsafe;

public class AccountThread extends Thread {
    //两个线程共享一个账户
    private Account act;

    //通过构造方法传递过来账户对象

    public AccountThread(Account act) {
        this.act = act;
    }

    @Override
    public void run() {
        //run方法的执行表示取款操作
        //假设取款5000
        double money = 5000;
        //取款
        act.withdraw(money);

        System.out.println(Thread.currentThread().getName() + "对" +act.getActno() + "取款成功,余额" + act.getBalance());
    }
}

5.5.1 在实例方法上使用synchronized

synchronized出现在实例方法上,一定锁的是this。

缺点:

  • 这种方法不灵活
  • 另一个缺点是:synchronized出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序执行的效率降低。这种方法不常用。

优点:

  • 代码量少,节俭了
  • 当共享对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方法。

5.5.2总结:

synchronized有三种写法:

第一种:同步代码块

​ 灵活

synchronized(线程共享对象){
			同步代码块;
		}

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

​ 表示共享对象一定是this
​ 并且同步代码块是整个方法体。

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

​ 表示找类锁。
​ 类锁永远只有1把。
​ 就算创建了100个对象,那类锁也只有一把。

​ 对象锁:1个对象1把锁,100个对象100把锁。
​ 类锁:100个对象,也可能只是1把类锁。

5.6.Java三大变量线程相关问题

实例变量:在堆中。

静态变量:在方法区。

局部变量:在栈中。

以上三大变量中:

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

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

  • ​ 局部变量在栈中。所以局部变量永远都不会共享。

  • 实例变量在堆中,堆只有1个。

  • 静态变量在方法区中,方法区只有1个。

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

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

  • 成员变量:可能会有线程安全问题。

6.1、如果使用局部变量的话:

建议使用:StringBuilder。

​ 因为局部变量不存在线程安全问题。选择StringBuilder。
​ StringBuffer效率比较低。

ArrayList是非线程安全的。
Vector是线程安全的。
HashMap HashSet是非线程安全的。
Hashtable是线程安全的。

5.7.死锁机制

死锁案例

package com.javase.Hashtable.Threadsafe;

public class DeadLock {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();

        //t1和t2两个线程共享o1,o2
        Thread t1 = new MyThread01(o1,o2);
        Thread t2 = new MyThread02(o1,o2);

        t1.start();
        t2.start();
    }
}

class MyThread01 extends Thread {
    Object o1;
    Object o2;

    public MyThread01(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        synchronized (o1) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2) {
            }
        }
    }
}

class MyThread02 extends Thread{
    Object o1;
    Object o2;

    public MyThread02(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }
    @Override
    public void run(){
        synchronized(o2){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(o1){
            }
        }
    }

}

死锁问题解决

第一种方案:

尽量使用局部变量代替“实例变量和静态变量”。

第二种方案:

如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,

对象不共享,就没有数据安全问题了。)

第三种方案:

如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。

6.守护线程

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

​ 一类是:用户线程

​ 一类是:守护线程(后台线程)

​ 其中具有代表性的就是:垃圾回收线程(守护线程)。

2.守护线程的特点:

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

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

守护线程用在什么地方呢?
每天00:00的时候系统数据 自动备份。
这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
一直在那里看着,没到00:00的时候就备份一次。所有的用户线程
如果结束了,守护线程自动退出,没有必要进行数据备份了。

package com.javase.Hashtable.Thread;

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 {
    @Override
    public void run() {
    int i = 0;
    while(true){
        System.out.println(Thread.currentThread().getName() + "----->" + (++i));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    }
}

7.定时器

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

例:每周要进行银行账户的总账操作。每天要进行数据的备份操作。

在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的,那么在java中其实可以采用多种方式实现:

  1. 可以使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行任务。这种方式是最原始的定时器。(比较low)
  2. 在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的。
  3. 在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。

8.实现线程的第三种方式

实现Callable接口。(JDK8新特性。)

这种方式实现的线程可以获取线程的返回值。之前讲解的那两种方式是无法获取线程返回值的,因为run方法返回void。

优点:可以获取到线程的执行结果

缺点:效率较低,在获取t线程执行结果的时候,当前线程受阻塞。

package com.javase.Hashtable.Thread;

import com.sun.javaws.IconUtil;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadTest06 {
    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 begin");
                Thread.sleep(1000 * 10);
                System.out.println("call end");
                int a = 100;
                int b = 200;
                return a + b; // 自动装箱为 Integer
            }
        });

        //创建线程对象
        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");
    }
}

9.Object类中的方法

关于Object类中的wait和notify方法。(生产者和消费者模式!)

第一:wait和notify方法不是线程对象的方法,

是java中任何一个java对象都有的方法,因为这两个方式是Object类中自带的。wait方法和notify方法不是通过线程对象调用,不是这样的:t.wait(),也不是这样的:t.notify()..不对。

第二:wait()方法作用

Object o = new Object();
		o.wait();

表示:让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。

o.wait();方法的调用,会让"当前线程(正在o对象上活动的线程)"进入等待状态。

第三:notify()方法作用

Object o = new Object();
		o.notify();

表示:唤醒正在o对象上等待的线程。

第四:notifyAll()方法:

这个方法是唤醒o对象上处于等待的所有线程。

9.1生产者和消费者模式

1.生产者和消费者的定义:

生产线程负责生产,消费线程负责消费

生产线程和消费线程要达到均衡

这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法

2.wait和notify方法不是线程对象的方法,是普通java对象都有的方法。

3.wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库。有线程安全问题。

4.wait方法作用:o.wait()让正在o对象上获取的线程t进入等待状态,并且释放掉之前占有的o对象的锁。

5.notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁。

代码实例:

模拟需求
仓库采用List集合
List集合中假设只能存储一个元素
1个元素就表示仓库满了
如果List集合中元素个数是0,表示仓库空了。
保证List集合中永远都是最多存储1个元素。
必须做到:生产1个消费1个
posted @   Gonfei  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 我与微信审核的“相爱相杀”看个人小程序副业
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示