1,线程的基本介绍:
1,什么是进程,什么是线程?
进程就是一个应用程序(一个进程是一个软件)。
线程是一个进程中的执行场景/执行单元。
一个进程可以开启多个线程。
2,对Java程序来说,当在DOS命令窗口中输入:
Java HelloWorld(就是执行一个Java文件)回车之后。
会先启动JVM,而JVM就是一个进程。
JVM再启动一个主线程调用main方法。
同时再启动一个垃圾回收线程负责看护,回收垃圾。
最起码,现在的Java程序中至少有两个线程并发。
一个垃圾回收线程,一个执行main方法的主线程。
3,进程和线程的关系:
进程可以看作是现实生活当中的公司。
线程可以看作是公司当中的某个员工。
注意:
进程A和进程B的内存独立不共享。
在Java语言当中:
线程A和线程B,堆内存和方法区内存共享。
但是栈内存独立,一个线程一个栈。
每个栈之间互不干扰,各自执行各自的,这就是多线程并发。
Java之所以有这种多线程机制,就是为了提高程序的处理效率。
4,使用了多线程机制之后,main方法结束之后,是不是,程序就结束了?
注意:main方法结束只是主线程结束,主栈空了,其他栈(线程)可能还在压栈弹栈。
5,对于单核CPU来说, 真的可以做到真正的多线程并发吗?
1,对于多核CPU电脑来说,真正的多线程并发是没有问题的。
每个CPU表示同一时间点上,可以真正的有多个线程并发执行。
2,什么是真正的多线程并发?
t1线程执行t1的。
t2线程执行t2的。
t1不会影响t2,t2也不会影响t1。这就叫做多线程并发
3,对于单核的CPU来说,是不能做到真正的多线程并发的,
但是可以做到给人一种多线程并发的感觉。
对于单核CPU来说,在某一个时间点,实际只能做一件事,但是
由于CUP的处理速度极快,多个线程之间频繁切换执行,给人的感觉就是:
多件事情在同时做。
2,线程的生命周期:
新建状态--就绪状态--运行状态--阻塞状态--死亡状态
3, 实现线程的第一种方式 :
编写一个类,之间继承java.lang.Thread,重写run方法。
以下代码我们需要注意:
1,start()方法的作用:启动一个分支线程,在JVM中开辟一个新的栈空间,
这段代码任务完成之后(新的栈空间开辟出来了),start方法瞬间就结束了。
线程就启动成功了,启动成功的线程会自动调用run方法,并且run方法在分支
栈的底部(压栈)。main方法在主栈的底部。它们两是平级的。
2,如果直接调用run方法是无法开启分支线程的,开启分支线程只能运行run方法。(目前是这样)该类中的其他方法没写在run方法当中,是无法实现的。
代码示例:
public class MyThread01 extends Thread{
@Override
public void run(){
//编写程序,这段程序运行在分支栈中
for (int i=0;i<100;i++){
System.out.println("分支线程--->"+i);
}
}
}
class Test{
public static void main(String[] args) {
//main方法,主线程
//创建一个分支线程对象
MyThread01 myThread01 = new MyThread01();
//启动线程,此处调用的是Thread中的方法
myThread01.start();
//如果直接调用
//这只是普通的方法调用,还是在主线程中,并不会开启分支线程
//myThread01.run();
//主线程运行的代码
for (int i=0;i<100;i++){
System.out.println("主线程--->"+i);
}
}
}
多线程内存示意图:
4, 实现线程的第二种方式 :
编写一个类,实现java.lang.Runnable接口,实现run方法。
1,java.lang.Runnable接口,只有以下一个方法:
public abstract void run();
要开始分支线程还是得借助Thread类的另外一个构造方法
参数是Runnable 类型(把实现Runnable接口的类传进去),
Thread类才有start方法开启分支线程。
2,java.lang.Thread类实现了这个接口,可以使用匿名内部类的方式直接开启分支线程。
代码示例:
1,不使用匿名内部类:
public class MyThread02 implements Runnable{
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("分支线程:"+i);
}
}
}
class Test01{
public static void main(String[] args) {
//创建线程对象
Thread thread = new Thread(new MyThread02());
//开启分支线程
thread.start();
for (int i=0;i<100;i++){
System.out.println("主线程:"+i);
}
}
}
2,使用匿名内部类:
使用匿名内部类,这里 可以 直接new接口
public class MyThread03 {
//主线程
public static void main(String[] args) {
//使用匿名内部类方式,这个类没有名字可以 直接new接口
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("分支线程:" + i);
}
}
});
//开启分支线程
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程:" + i);
}
}
}
5, 实现线程的第三种方式 :
编写一个类实现Callable接口。(JDK8新特性),重写call方法。
这种方式实现的线程可以获取线程的返回值。
上面两种方法都是无法获取线程返回值的,
如果系统委派一个线程去执行一个任务,该线程执行完成后,可能会有一个执行结果,
这时候就可以使用这个接口。
部分源码:
可以看出FutureTask是Runnable的子类:可以使用Thread类的构造方法创建分支线程
public class FutureTask<V> implements RunnableFuture<V>{}
public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }
public FutureTask(Callable<V> callable){}其中一个构造方法。
1,没有使用匿名内部类代码示例:
public class MyThread04 implements Callable{
@Override
public Object call() throws Exception {
//call方法相当于run方法,只不过这个有返回值
System.out.println("call begin");
System.out.println(new Date());
Thread.sleep(1000);//线程休眠,后面会讲
System.out.println(new Date());
System.out.println("call end");
int a=100;
int b=100;
return a+b;//自动装箱
}
}
class Test03{
public static void main(String[] args) {
//第一步创建一个“未来任务类”对象
FutureTask futureTask = new FutureTask(new MyThread04());
//创建线程对象
Thread thread = new Thread(futureTask);
//启动线程
thread.start();
//怎么把分支t线程的返回结果
//该方法需要处理异常
//如果此方法后面还有代码必须得等到拿到结果,就是线程执行完
Object o = futureTask.get();
}
}
1,使用匿名内部类代码示例:
public class MyThread05 {
public static void main(String[] args) {
FutureTask futureTask = new FutureTask(new Callable(){
@Override
public Object call() throws Exception {
//call方法相当于run方法,只不过这个有返回值
int a=100;
int b=100;
return a+b;//自动装箱
}
});
//创建线程对象
Thread thread = new Thread(futureTask);
//开启线程
thread.start();
}
}
6,获取/修改当前线程的名字:
使用以下Thread类中的三个方法:
.setName()//修改名字
.getName()//获取当前线程名字
.currentThread()//在哪个线程方法中,就获取哪个线程对象
默认名字:Thread-0
Thread-1
.....
代码示例:
1, .setName()//修改名字
.getName()//获取当前线程名字
public class MyThread06 {
public static void main(String[] args) {
//创建线程对象
MyThread01 t1= new MyThread01();
//没有设置线程名字
System.out.println(t1.getName());//Thread-0
//手动设置线程名字
t1.setName("我是当前线程");
System.out.println(t1.getName());//我是当前线程
MyThread01 t2 = new MyThread01();
System.out.println(t2.getName());//Thread-1
}
}
2,.currentThread()
在哪个线程方法中,就获取哪个线程对象:
可以用来获取主线程对象。
public class MyThread07 {
public static void main(String[] args) {
Test04 t1= new Test04();
t1.setName("t1");
System.out.println(t1.getName());
Test04 t2 = new Test04();
t2.setName("t2");
System.out.println(t2.getName());
t1.start();
t2.start();
//获取当前线程对象---也就是main线程的对象
//this.getName()这种方式是不行的
Thread thread = Thread.currentThread();
System.out.println("主线程的名字:"+thread.getName());
}
}
class Test04 extends Thread{
@Override
public void run() {
//哪个线程调用run方法就是创建的哪个线程对象
Thread thread = Thread.currentThread();
System.out.println("分支线程的名字:"+thread.getName());
}
}
结果:
t1
t2
主线程的名字:main
分支线程的名字:t1
分支线程的名字:t2
7,休眠方法:
使用sleep方法:
1,源码
public static native void sleep(long millis) throws InterruptedException;
可以看出是一个静态方法,直接使用类名就可以调用,参数是毫秒,底层是用C++实现
作用:
让当前进程进入休眠,进入阻塞状态,放弃占有的CPU时间片,让给其他线程使用
可以做到每隔几秒运行一段代码。
需要注意的是:
此方法是让当前进程进入阻塞状态,即使使用非本线程的对象使用此方法,依旧是让本线程进入阻塞状态。
代码示例:
public class MyThread08 {
public static void main(String[] args) {
try {
//获取执行到此的时间
System.out.println(new Date());
//隔五秒运行下面的代码,需要处理异常
Thread.sleep(5000);
//获取执行到此的时间
System.out.println(new Date());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果:
Thu Sep 22 14:25:34 CST 2022
Thu Sep 22 14:25:39 CST 2022
注意以下代码可以发现:
1,sleep方法是让当前进程进入阻塞状态,即使使用非本线程的对象使用此方法,依旧是让本线程进入阻塞状态。
2,还要注意一点,在run方法中使用sleep方法只能使用try--catch方式处理异常,不能抛出
因为这个方法是继承重写的方法,在父类这个方法是没有异常处理的
public class MyThread09 {
public static void main(String[] args) {
Thread t1 = new Thread(new Test05());
t1.start();
try {
System.out.println("这里是主线程前:"+new Date());
t1.sleep(5000);//注意这里不是把t1分支线程休眠,而是主线程
System.out.println("这里是主线程后:"+new Date());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Test05 implements Runnable{
@Override
public void run() {
Thread thread = Thread.currentThread();
try {
System.out.println("这里是分支线程前:"+new Date());
thread.sleep(5000);
System.out.println("这里是分支线程后:"+new Date());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果:
这里是分支线程前:Thu Sep 22 14:38:53 CST 2022
这里是主线程前:Thu Sep 22 14:38:53 CST 2022
这里是分支线程后:Thu Sep 22 14:38:58 CST 2022
这里是主线程后:Thu Sep 22 14:38:58 CST 2022
1,中断休眠:
.interrupt();
终止t线程的睡眠(这种中断睡眠的方式已考虑java的异常机制)
还有一个.stop方法;
通过线程对象调用,把线程强行五秒后终止。(不推荐使用,已过时)
public class MyThread01 {
public static void main(String[] args) {
Thread t1 = new Thread(new Test01());
t1.start();
//终止t线程的睡眠(这种中断睡眠的方式已考虑java的异常机制)
t1.interrupt();//干扰,
try {
System.out.println("这里是主线程前:"+new Date());
t1.sleep(5000);//注意这里不是把t1分支线程休眠,而是主线程
System.out.println("这里是主线程后:"+new Date());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Test01 implements Runnable{
@Override
public void run() {
Thread thread = Thread.currentThread();
try {
System.out.println("这里是分支线程前:"+new Date());
thread.sleep(5000);
System.out.println("这里是分支线程后:"+new Date());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程调度:
线程调度就是操作系统为线程分配处理器使用权的过程。
1,常见的线程调度模型:
抢占式调度模型:
哪个线程的优先级比较高,强到的CPU时间片的概率就高一些/多一些
这里的多一些是指”运行状态“的时间更长
Java采用的就是抢占式调度模型
均分式调度模型:
平均分配CPU时间片。每个线程占有的时间片的时间长度一样,概率一样
优先级的设置:
public final void setPriority(int newPriority)
设置线程的优先级
public final int getPriority() { return priority; }
获取线程的优先级
优先级最低是1,最高时10,默认时5
优先级比较高的获取CPU时间片可能会多一些。(但不是一定,大概率是高的)。
public class MyThread02 {
public static void main(String[] args) {
Thread t1= new Thread(new Test());
t1.setName("t1");
//设置优先级
t1.setPriority(10);
//获取主线程对象
Thread t2= Thread.currentThread();
//获取主线程的优先级
System.out.println(t2.getPriority());
//开启线程
t1.start();
for (int i=0;i<100;i++){
System.out.println(t2.getName()+":"+i);
}
}
}
class Test implements Runnable{
@Override
public void run() {
for (int i=0;i<100;i++){
//获取使用run方法线程名字
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
让位方法:
源码:
public static native void yield();
是一个静态方法,作用是让当前正在执行的线程对象暂停,并执行其它线程。
注意 :该方法不是阻塞方法,它是让当前线程让位,让给其他线程使用,
使当前线程从“运行状态”进入“就绪状态”
但是在回到就绪状态之后,又可能还会再次强到CPU时间片,进入运行状态。
多线程并发/线程同步:
1,关于多线程并发环境下,数据的安全问题:
以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,对象的创建,线程的启动等,都已经实现完了。这些代码我们不需要编写,所以我么更需要注意的是,把自己编写的程序放到多线程的环境下运行,数据安全在多线程并发环境下是否是安全的。
2,什么时候数据在多线程并发的环境下会出现安全问题?
三个条件:
1,多线程并发
2,有共享数据
3,共享数据有修改行为
3,怎么解决线程安全问题?
1,线程排队执行(不能并发)。这种机制被称为“线程同步机制”
2,线程同步就是线程排队了,线程排队是会牺牲一部分效率的,没办法,数据安全是第一位的,只有做到数据安全,才考虑效率问题。
两个专业术语:
异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,
谁都不需要等谁,这种编程模型叫做:异步编程模型。
其实就是多线程并发(效率较高)
同步编程模型:
线程t1和线程t2,各自执行各自的,在t1执行的时候必须等t2执行结束,或者相反。
两个线程之间发生了等待关系,这就是同步编程模型。
效率较低,但是数据安全。就是线程排队执行
并发和并行的区别:
并发性(concurrency),又称共行性,是指能处理多个同时性活动的能力,并发事件之间不一定要同一时刻发生。 并行(parallelism)是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行
以一个简单的取钱例子说明并发带来的问题:
有一个只有10000块的账户,取了两次5000,还剩5000
建立一个账户类:
public class Account {
//账号
private String actor;
//余额
private double balance;
//取款的方法
public void withdraw(double money){
//t1和t2并发这个方法,(t1和t2是两个栈。两个栈操作同一个对象)
//取款之前的余额
double before=this.getBalance();
//取款之后的余额
double after=before-money;
//更新余额
//如果t1线程执行到了这里,但是还没来的及更新,t2线程就进来withdraw(取款了)
//这个时候,就会出现取钱出现问题
this.setBalance(after);
System.out.println("还剩下余额:"+after);
}
public Account() {
}
public Account(String actor, double balance) {
this.actor = actor;
this.balance = balance;
}
public String getActor() {
return actor;
}
public void setActor(String actor) {
this.actor = actor;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Account{" +
"actor='" + actor + '\'' +
", balance=" + balance +
'}';
}
}
线程类(就是可同时发生的行为):
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);
}
}
测试结果:
注意,并不是一定会出现下面的情况,只是可能会出现
public class MyThread04 {
public static void main(String[] args) {
//创建一个对象
Account act = new Account("act-001", 10000);
//创建两个线程
Thread t1 = new AccountThread(act);
Thread t2 = new AccountThread(act);
t1.setName("t1");
t2.setName("t2");
//启动线程
t1.start();
t2.start();
}
}
结果:
还剩下余额:5000.0
还剩下余额:5000.0
解决办法:
线程同步机制:
使用线程同步机制的语法:
synchronized(){
//线程同步代码块
}
使用这个方法之后,必须得等到第一个进入的线程 完成之后,第二个线程才能进入(得来了解Java中锁得概念,看后面)
synchronized后面的小括号中传的这个-数据-是相当关键的,这个数据必须是多线程共享的数据。才能达到多线程排队。
()中写什么?
这要看你想要哪些线程同步。
假设t1,t2,t3,t4,t5,有五个线程
你只希望t1 t2, t3排队, t4, t5 不需要排队,怎么办?
你一定要在()中写一个t1 t2, t3共享的对象,而这个对象对于t4, t5不共享
我们上面的例子共享对象是:账户对象,账户对象是共享的,那么this就是账户对象,
不一定是this,这里只要是多线程共享的哪个对象就行。
如下面部分代码:
1,同步代码块,写在代码块外面,里面的共享对象可以自己定
//取款的方法
public void withdraw(double money) {
// synchronized (actor)//可以使用实例变量,
// 当一个对象是共享时,它的实例变量也是共享的
synchronized (this){
//t1和t2并发这个方法,(t1和t2是两个栈。两个栈操作同一个对象)
//取款之前的余额
double before = this.getBalance();
//取款之后的余额
double after = before - money;
//更新余额
//如果t1线程执行到了这里,但是还没来的及更新,t2线程就进来withdraw(取款了)
//这个时候,就会出现取钱出现问题
this.setBalance(after);
System.out.println("还剩下余额:" + after);
}
}
1.2 这种方式会扩大同步的范围,效率更低了
@Override
public void run() {
//run方法的执行表示取款操作。
//假设取款5000
double money=5000;
//取款
//多线程并发执行这个方法
synchronized (act) {
act.withdraw(money);
}
}
2,在实例方法上使用synchronized
注意:这种方式表示共享对象一定时this,并且同步代码块是整个方法体。
public synchronized void withdraw(double money) {
// synchronized (actor)//可以使用实例变量,
// 当一个对象是共享时,它的实例变量也是共享的
//synchronized (this){
//t1和t2并发这个方法,(t1和t2是两个栈。两个栈操作同一个对象)
//取款之前的余额
double before = this.getBalance();
//取款之后的余额
double after = before - money;
//更新余额
//如果t1线程执行到了这里,但是还没来的及更新,t2线程就进来withdraw(取款了)
//这个时候,就会出现取钱出现问题
this.setBalance(after);
System.out.println("还剩下余额:" + after);
}
Java“锁”的概念:
在Java语言中,任何一个对象都有一把“锁”,其实这把锁就是标记(只是把它叫做锁)
100个对象,100把锁,一个对象一把锁。
以下代码的执行原理?
1,假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
2,假设t1先执行了,遇到synchronized,这个时候自动找“后面共享对象”的对象锁,
找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中,一直
都是占有这把锁的。直到同步代码块代码执行结束,这把锁才会释放
3,假设t1已经占有这把锁了,此时t2也遇到synchronized,也会去自动找
“后面共享对象”的对象锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束
直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁了,
然后t2占有这把锁,进入同步代码块的执行。
这样就达到了线程排队执行。
这里需要注意的是:这个共享对象一定的选好了。这个共享对象一定是你需要排队执行
的这些线程对象所共享的。
注意:如果一个对象时共享的,那么对象里面的实例变量也是共享的。
锁池的概念:
遇到synchronized会在锁池里面找共享对象的锁。
线程进入锁池找共享对象的时候,会释放之前占有的CPU时间片,有可能找到了,有肯没有找到,没找到则在锁池中等待,如果找到了会进入就绪状态继续抢占CPU时间片
总结:
synchronized的三种写法:
第一种:同步代码块
灵活
synchronized(){
//线程同步代码块
}
第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体
第三种:在静态方法上使用synchronized
表示找类锁
类锁永远只有一把。
就算创建了100个对象,那类锁也只有一把
(区别看下面的截图)
对象锁:一个对象一把锁,100个对象100把锁
类锁:100个对象,也可能是一把类锁。
对象锁:
类锁:
Java变量的线程安全问题:
实例变量:在堆中。
静态变量:在方法去中。
局部变量:在栈中。
局部变量永远不会存在线程安全问题
因为局部变量不共享。
实例变量和静态变量是有可能会出现线程安全问题的,它们多线程是共享的
守护线程:
Java语言中线程分为两大类:
1,用户线程
2,守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,
守护线程自动结束
注意:主线程main方法是一个用户线程。
用户线程一般用在什么地方?
假设每天00:00的时候系统数据自动备份。
这个时候需要使用到定时器,并且我可以将定时器设置为守护线程。
一直在那里看着,每到00:00的时候就备份一次。所有的用户线程
如果结束了,守护线程自动退出,没有必要进行数据备份了。
守护线程的创建 :
只需要在启动线程之前,使用方法“对象.setDaemon(ture)”,就可以把线程设置为守护线程