java的多线程
一、多线程的概述
1.什么是进程,什么是线程?
进程:是一个应用程序(一个软件).
线程:是一个进程中的 执行场景 / 执行单元.
2.线程和线程之间
进程a和进程内存不会独立
线程a和线程b,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈.
多线程并发:几个线程就有几个栈空间,互不干扰
3.问题
使用多线程机制后,main方法结束,是不是有可能程序也不会结束,main方法结束只是主线程结束了,主栈空了,其他的栈(线程)可能还在运行.
4.多线程并发的理解
对于多核cpu实现多线程并发是可以的.
对于单核cpu真的可以实现多线程并发嘛?
单核无法真的做到多线程并发,但是可以给人一种多线程并发的感觉.先处理一下a线程,再处理b线程,再处理a线程,反复交替,给人以多线程并发的错觉.
二、实现线程的第1种方式
java支持多线程机制,并且java已经将多线程实现了,我们只需要继承就可以了.
第一种方式:编写一个类,直接继承 java.lang.Thread,重写run方法
public class one {
public static void main(String[] args) {
thr thr = new thr();//创建一个线程分支对象
thr.start();
//start()方法作用在在jvm中开新的栈空间,这段代码的任务就是开辟栈空间,线程就启动成功了
// 启动成功的线程会自动调用run方法。并且run方法处于分支栈的底部(压栈)
//main在主栈的栈底部,run和main是平级别的
//如果直接调用对象的run方法不会分配栈空间,依旧在同一个栈里面(说白了单线程)
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
class thr extends Thread{
@Override
public void run() {
//编写程序,这段程序就是在分支栈中运行
for (int i = 0; i < 10 ; i++) {
System.out.println(i + "分支");
}
}
}
三、实现线程的第二种方式
编写一个类,实现java.lang.Runnable接口,实现run方法
public class one {
public static void main(String[] args) {
//创建一个可运行的对象
thr thr = new thr();
//将可运行的对象封装为一个线程对象,
// 因为Thread的构造方法可以接收一个实现了Runnable的类
Thread thread = new Thread(thr);
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
//这并不是一个线程类,是一个可运行的类,还不是一个线程
class thr implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10 ; i++) {
System.out.println(i + "run");
}
}
}
采用匿名内部类的方式
// 里面的 new Runnable()是通过一个没有名字的类new的对象
// 接口无法new对象
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
});
thread.start();
四、线程的生命周期
新建:采用 new语句创建完成
就绪:执行 start 后
运行:占用 CPU 时间
阻塞:执行了 wait 语句、执行了 sleep 语句和等待某个对象锁,等待输入的场合
终止:退出 run()方法
五、获取线程对象的名字
获取当前线程对象
获取线程对象的名字
修改线程对象的名字
线程对象名字的操作
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println( "分支线程>>" + i);
}
}
});
//获取线程名字
String s = thread.getName();
System.out.println(s); // Thread-0
//修改线程的名字
thread.setName("jack");
System.out.println(thread.getName()); // jack
thread.start();
当线程没有设置名字的时候,默认的名字的规律
Thread-0
Thread-1
Thread-2
…
六、获取当前线程对象
方法:
static Thread currentThread()
// 返回对当前正在执行的线程对象的引用
// 代码在哪里获取到的就是哪个线程,感觉上像this
// 获取当前线程对象
// 这个方法出现在main方法中,所以当前线程就是主线程
Thread thread = Thread.currentThread();
System.out.println(thread.getName()); // main
Thread ty = new Thread(new Runnable() {
@Override
public void run() {
// 获取当前线程,当前线程表示的就是分支线程
System.out.println(Thread.currentThread().getName());// Thread-0
}
});
ty.start();
七、线程sleep方法
static void sleep(long millis)// 参数的毫秒
// 作用:让当前的线程进入休眠,进入“阻塞”状态,放弃占有的cpu时间片
Thread.sleep()
Thread.sleep()方法的面试题
Thread thread = Thread.currentThread();
System.out.println(thread.getName()); // main
Thread ty = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "--->"+i);
}
}
});
ty.start();
try {
ty.sleep(1000 * 2); // 会让线程进入休眠状态吗?
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("enmmmm");
不会,因为sleep()方法是一个静态方法和调用的对象没有关系
ty.sleep(1000 * 2); 在执行的时候还是会转化为 Thread.sleep(1000);来执行
让当前的线程也就是main线程休眠
通过异常的方式唤醒sleep
public class one {
public static void main(String[] args) {
run run = new run();
Thread thread = new Thread(run);
thread.start();
// 希望2秒过后开始执行
try {
Thread.sleep(1000 * 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 醒来(这种中断睡眠的方式是通过异常处理机制)
thread.interrupt();
}
}
class run implements Runnable{
@Override
public void run() {
System.out.println("start");
// 只能够try-catch,无法上抛是因为,子类不能够比父类抛出更加宽泛的异常
// 总结:run方法在父类中没有抛出异常,子类只能够try-catch无法上抛
try {
Thread.sleep(1000 * 60 * 60 * 24); // 睡眠一天
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end");
}
}
强行终止线程
run run = new run();
Thread thread = new Thread(run);
thread.start();
// 希望2秒过后开始执行
try {
Thread.sleep(1000 * 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 醒来(强行终止thread线程)
thread.stop();//已经过时,不建议使用
缺陷:容易丢失数据,方法会直接将线程终止
合理的终止线程的执行(常用)
public class one {
public static void main(String[] args) {
run run = new run();
Thread thread = new Thread(run);
thread.start();
// 希望3秒过后开始执行
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 醒来
run.runs = false; // 通过修改判断条件来终止睡眠
}
}
class run implements Runnable{
// 打一个标记
boolean runs = true;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (runs){
System.out.println(Thread.currentThread().getName() + "-->" + i);
try {
Thread.sleep(1000); // 睡眠一秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
// 终止当前的线程
return;
}
}
}
}
八、线程的调度(了解)
1.常见的线程调度模型
抢占式调度模型: 那个线程的优先级高,抢到的cpu时间片的概念就高些/多一些
java使用的就是抢占式调度模型.
均分式调度模型: 平均分配cpu时间片,每个线程占有的cpu时间片长度一样,平均 分配,一切平等.
2.和调度有关的方法
实例方法:
void setPriority(int newPriority) 设置线程的优先级
int getPriority() 获取线程的优先级
最低的优先级是:1
默认优先级是:5
最高的是:10
void join()
合并线程
run run = new run();
Thread thread = new Thread(run);
try {
thread.join();
// 当前的线程进入阻塞,thread线程执行,直到thread线程执行结束,当前线程才可以执行
} catch (InterruptedException e) {
e.printStackTrace();
}
静态方法:
static void yield() 让位方法
暂停当前正在执行的线程对象,让给其他线程,将线程的“运行状态”转变为“就绪状态”
当然让位过后还有可能又抢到了时间片
3.线程的优先级
优先级较高的抢到的cpu时间片相对多一些
System.out.println(Thread.MAX_PRIORITY); // 最高
System.out.println(Thread.NORM_PRIORITY); // 默认
System.out.println(Thread.MIN_PRIORITY); // 最低
Thread thread = Thread.currentThread();
System.out.println("获取当前线程的优先级" + thread.getPriority()); // 5
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
//获取线程的优先级
System.out.println("分支的默认线程优先级" + Thread.currentThread().getPriority());//5
}
});
thread1.start();
4.线程的让位
当前的线程暂停,回到就绪状态,让给其他线程
静态方法: Thread.yield()
for (int i = 0; i < 100; i++) {
System.out.println("main" + i);
if (i % 2 == 0){
Thread.yield(); // 线程让位
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"--"+ i);
}
}
});
thread1.start();
5.线程合并
System.out.println("start");
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 6; i++) {
System.out.println(Thread.currentThread().getName()+"--"+ i);
try {
Thread.sleep(1000 * 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread1.start();
try {
thread1.join(); // thread1 合并到当前的线程中,当前线程收阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end");
九、线程安全的问题
开发中,项目运行在服务器中,而服务器已经将线程创建和启动都实现了。
不需要编写,需要关注的是数据放入到多线程并发的环境下是否安全(重点)
1.什么时候数据在多线程的环境下操作安全问题
三个条件:
多线程并发
有共享数据
共享数据有修改的行为
2.怎么解决线程安全问题
线程排队执行解决线程安全问题,被称为“线程同步机制”
线程同步会牺牲效率,以安全为主
十、同步和异步的理解
异步编程模型:
两个线程各自执行自己的,谁也不用等谁,其实就是多线程并发效率高
同步编程模型:
两个线程在执行的时候,t1在执行的时候,必须等待t2线程的执行,两个线程间发生了等待关系,这就是同步编程模型
总结:异步就是并发,同步就是排队
模拟银行取款
此方法模拟的情况是当两个线程对同一个对象进行操作的时候,当两者都调用对象的时候,用户1已经取款但是余额没有刷新,这个时候巧的是用户2也在取款,并且成功取出的情况,两者取出的金额大于余额。
定义银行账户
class Account{
private String actne; // 账号
private double balance; // 余额
public Account(String actne, double balance) {
this.actne = actne;
this.balance = balance;
}
public Account() {
}
// 取款方法
public void get_money(double d1){
if (d1 > getBalance()){
System.out.println("取款大于余额,无法取款");
return;
}
double his = getBalance();
setBalance(getBalance() - d1);
System.out.println("取款成功记录");
System.out.println("--------------------");
System.out.println("取款前" +his);
System.out.println("取出" + d1);
System.out.println("剩余" + getBalance());
}
public String getActne() {
return actne;
}
public void setActne(String actne) {
this.actne = actne;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
模拟多个线程对银行账号进行操作
// 账号对象
Account account = new Account("jack",1000);
// 两个栈操控堆中的一个对象
Thread1 thread1 = new Thread1(account); // 一个人对账户操作
thread1.setName("t1用户");
Thread1 thread2 = new Thread1(account); // 另外一个人对账户操作
thread2.setName("t2用户");
thread1.start();
thread2.start();
十一、同步代码块synchronized
线程同步机制的语法
// synchronized 后面的小括号传入的数据非常关键,这个数据必须是多线程共享的数据,才能够达到多线程排队
// () 里面写什么取决于需要哪些线程同步,将需要同步的线程的数据写入
synchronized(){
// 线程同步代码块
}
关于上面银行取款问题的解决方案
public void get_money(double d1){
// 将以下这几行代码必须排队执行,不能够并发
// 一个线程执行后,另外一个线程执行
synchronized(this){
if (d1 > getBalance()){
System.out.println("取款大于余额,无法取款");
return;
}
double his = getBalance();
setBalance(getBalance() - d1);
System.out.println(Thread.currentThread().getName() + "取款成功记录");
System.out.println("--------------------");
System.out.println("取款前" +his);
System.out.println("取出" + d1);
System.out.println("剩余" + getBalance());
}
}
两个线程对同一个账户对象,进行操作所以可以填入this表示当前账户
锁
在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记(只是把它叫做锁)
100个对象,100把锁,一个对象一把锁
当两个线程执行同一个代码块的时候,肯定存在一个先后关系,当其中一个线程占到先机后遇见synchronized,就给同步代码块上一把锁,这个对象执行不结束锁就无法打开,其余的线程就无法进来.
synchronized的理解(用法,以及可以填入的数据)
在账户里面创建一个实例对象
在账户对象中添加一个Object 类型的对象为实例变量(Account对象是多线程共享的,Account对象中的实例变量也是共享的)
synchronized() 的括号里面只需要放一个共享对象的对象锁就可以,所以也可以将Accoutn账户类里面的一个对象传进来.
填入字符串
synchronized(字符串),因为字符串定义在常量池当中
但是写字符串的话所有的线程都会同步
this
所以this的情况,如果是同一个账户synchronized(this)使用this修饰的话,当两个线程对同一个账户进行操作时候存在排队的情况,但是第三个线程对一个新的账户进行操作的时候可以轻松访问,不会存在影响
this的总结:使用this,不同的线程对同一个账户操作的时候会存在锁的情况
哪些变量有线程安全问题
三大变量
实例变量:堆中
静态变量:方法区
局部变量:栈中
以上三大变量中:局部变量和常量永远也不会存在线程安全问题,因为局部变量不共享。(一个线程一个栈,而局部变量又在栈中)
实例变量在堆中,堆只有一个。
静态变量在方法区中,方法区只有一个
堆和方法区都是多线程共享的,可能存在线程安全问题。
synchronized扩大同步范围
// 账户对象
class Thread1 extends Thread{
Account account;
public Thread1(Account account) {
this.account = account;
}
public Thread1() {
}
@Override
public void run() {
synchronized (account){
account.get_money(1000);
}
}
}
将账户对象的run方法里面的调用取款的方法包裹起来,也可以实现多线程同步
但是扩大了范围,效率更加低了,
而且不能够使用this,因为this表示的是当前的线程对象,当前存在两个线程对象对一个账户进行操作
synchronized出现在实例方法上面
synchronized 出现在实例方法上面的锁一定是this,
缺点:
缺乏灵活性
synchronized 出现在实例方法上面表示整个方法体都需要同步,会无故扩大同 步的范围,导致程序的效率降低
优点:
代码少
// 取款方法
public synchronized void get_money(double d1){
if (d1 > getBalance()) {
System.out.println("取款大于余额,无法取款");
return;
}
double his = getBalance();
setBalance(getBalance() - d1);
System.out.println(Thread.currentThread().getName() + "取款成功记录");
System.out.println("--------------------");
System.out.println("取款前" + his);
System.out.println("取出" + d1);
System.out.println("剩余" + getBalance());
}
总结:synchronized的三种写法
第一种:同步代码块
比较灵活
synchronized(){
}
第二种:在实例方法上面使用synchronized
表示共享的对象一定是this
并且同步代码块在整个方法体
第三种:在静态方法上使用synchronized
表示找类锁
类锁永远只有一把
就算创建了无数个对象,也可能只是一把锁
十二、死锁
会让程序停止,不会出现任何异常和错误,也不会结束运行
要求会写死锁,会写才会注意到这个错误
public class one {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
thread1 thread1 = new thread1(o1,o2);
thread2 thread2 = new thread2(o1,o2);
thread1.start();
thread2.start();
}
}
class thread1 extends Thread{
Object o1;
Object o2;
public thread1(Object o1, Object o2){
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o1){
//睡眠一秒,为确保往下面执行的时候o2已经上锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class thread2 extends Thread{
Object o1;
Object o2;
public thread2(Object o1, Object o2){
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o2){
synchronized (o1){
}
}
}
}
十三、开发中解决线程问题
synchronized 会然程序的执行效率降低,用户体验不好,系统的用户吞吐量降低,在
不得已的情况下选择线程同步机制
第一种方案:尽量使用局部变量代替“实例变量和静态变量”
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了,(一个线程对应一个对象,100个对象对应100个对象,对象不共享,就,就没有数据安全问题了)
第三种方案:如果不能够使用局部变量,对象也不能够创建多个,这个时候就只能够选择synchronized了,线程同步机制.
十四、守护线程
java里面的线程分类,
分为用户线程(以上学的包括主线程)
守护线程(后台线程,比如垃圾回收机制)
守护线程的特点:
一般守护线程是一个死循环
用户线程结束,守护线程自动结束
守护线程用在什么地方?
每天在指定时间系统自动备数据。
这个需要使用定时器,并且我们可以将定时器设置为守护线程。
案例演示:
public class one {
public static void main(String[] args) {
Dath dath = new Dath();
dath.setName("备份数据");
// 启动线程之前,将线程设置为守护线程
// 作为守护线程,当主线程执行完毕后,虽然run方法里面操作whi(true)
// 但是由于设置了守护线程,当需要守护的线程结束运行后,也会结束运行,
// 如果没有设置守护线程,就会一种执行下去.
dath.setDaemon(true);
dath.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 Dath 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();
}
}
}
}
十五、定时器
间隔特定的时间,执行特定的程序。
java实现可以采用多种方式实现:
1)可以使用sleep()设置睡眠时间,没到指定的时间点开始执行任务
2)java的类库中写好了一个java.util.Timer,可以直接拿来用,目前开发中很少用,高级的框架都是支持定时任务的
void schedule(TimeTask task, Date firstTime, long period)
// 定时任务类型 定时任务第一次执行时间 延迟多久执行一次
// 安排指定的任务在指定的时间内开始进行重复的固定延迟执
TimeTask 实现了 Runnable接口,是一个线程
TimeTask 是一个抽象类无法创建对象
案例演示:
public class one {
public static void main(String[] args) {
// 创建定时器对象
Timer t1 = new Timer();
//Timer t2 = new Timer(true); 表示是以守护线程的方式
//指定定时任务
Date firsttime = null;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
firsttime = simpleDateFormat.parse("2021-02-20 20:14:00");
} catch (ParseException e) {
e.printStackTrace();
}
t1.schedule(new logtim(), firsttime,10000);
}
}
class logtim extends TimerTask{
@Override
public void run() {
// 编写需要执行的任务
System.out.println("定时任务执行");
}
}
十六、实现线程的第三种方式:
FutrueTask方式,实现Callable接口(JDK8新特性)
优点:
这种方式实现的线程可以获取线程的返回值,之前的方式无法获取返回值
缺点:
通过调用get方法获取其他线程的执行结果的时候,当前线程收阻塞,效率较低.
代码演示:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask; // JUC包下的,属于java的并发包
public class one {
public static void main(String[] args) {
// 第一步:
// 创建一个实现了Callable接口的类的对象
myCallable aaaa = new myCallable();
// 第二步:创建一个未来任务类对象
FutureTask task = new FutureTask(aaaa); // 参数传入一个Callable接口的实现对象
// 第三步:创建线程对象
Thread t = new Thread(task);
// 启动线程
t.start();
// 在main主线程中获取t线程的结果
try {
Object ob = task.get();
// get方法的执行会导致mian主程序受阻,下面的代码会等待get的结果
// 而get的方法可能需要很久,因为是从另一个线程获取数据
System.out.println(ob);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
// 也可以使用匿名内部类的方式
// Callable 是一个接口
class myCallable implements Callable{
// call方法相当于run方法,只不过有返回值
@Override
public Object call() throws Exception {
System.out.println("开始执行");
return "执行完毕";
}
}
十七、Object类中的wait()和notify()方法(生产者消费者模式)
1.wait()和notify()不是线程对象的方法,是java中任何一个对象都有i的方法,是Object自带的
2.wati方法的作用?
让调用该方法上面活动的线程进入等待状态,无期限等待,直到被唤醒
3.notify方法作用?
唤醒调用notify()的线程上的的等待的线程
notifyAll ()唤醒调用notify的线程上所有线程
4.生产者消费者模式
仓库是共享的,使用仓库来调用wait和notify方法
生产线程和消费线程达到均衡
wait和notufy建立在线程同步的基上,因为多线程同时操作一个仓库,有线程安全问题
生产一个消费一个
// 仓库类表示存储的数据,使用list集合存储数据,
// list里面数据个数为0时,表示仓库空了,保证做到这种效果:生成一个消费一个
public class one {
public static void main(String[] args) {
// 创建集合表示仓库
List list = new ArrayList();
// 生产者
Thread thread1 = new Thread(new Producer(list));
// 消费者
Thread thread2 = new Thread(new Consumer(list));
thread1.start();
thread2.start();
}
}
// 生产线程
class Producer implements Runnable{
private List lists;
public Producer(List lists) {
this.lists = lists;
}
public Producer() {
}
@Override
public void run() {
// 一直生产
while (true){
// 给仓库list加锁
synchronized (lists){
if (lists.size() > 0){ // 表示仓库存在,不需要生产
// 将当前的生产线程睡眠
try {
lists.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 为仓库添加数据
Object o1 = new Object();
lists.add(o1);
System.out.println(Thread.currentThread().getName() + "-生产->" + o1);
lists.notifyAll();
}
}
}
}
// 消费者线程
class Consumer implements Runnable{
private List lists;
public Consumer(List lists) {
this.lists = lists;
}
public Consumer() {
}
@Override
public void run() {
// 一直消费
while (true){
synchronized (lists){
if (lists.size() == 0){
// 表示仓库里面已经没有了。需要生产线程生产
try {
lists.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 仓库里面有数据进行消费
lists.remove(0);
System.out.println("消费...");
lists.notifyAll();
}
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 为仓库添加数据
Object o1 = new Object();
lists.add(o1);
System.out.println(Thread.currentThread().getName() + "-生产->" + o1);
lists.notifyAll();
}
}
}
}
// 消费者线程
class Consumer implements Runnable{
private List lists;
public Consumer(List lists) {
this.lists = lists;
}
public Consumer() {
}
@Override
public void run() {
// 一直消费
while (true){
synchronized (lists){
if (lists.size() == 0){
// 表示仓库里面已经没有了。需要生产线程生产
try {
lists.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 仓库里面有数据进行消费
lists.remove(0);
System.out.println("消费...");
lists.notifyAll();
}
}
}
}