多线程
概述
什么是进程?什么是线程?
答:答:进程是资源分配的基本单位;线程是运行的基本单位、是调度的基本单位。
可以简单理解为,进程是一个应用程序(相当于软件)。线程是一个进程中的执行场景/执行单元,一个进程可以启动多个线程。
对于java 程序来说当在DOS命令窗口中输入java HelloWorld
回车之后,会先启动JVM(进程),JVM再启动一个主线程调用main方法,同时再启动一个垃圾回收线程负责看护且回收垃圾。
不同进程间的内存独立不共享;不同线程间堆内存和方法区内存共享,但是栈内存相互独立,一个线程一个栈。
问题:使用了多线程机制后,main方法结束,有可能程序也不会结束,main方法的主线程结束后其他的栈(线程)可能还在压栈弹栈。
对于单核的 cpu 可以做到真正的多线程并发吗?
真正的多线程并发:各个线程同时工作互不影响。(比如火车站的售票窗口)
单核的 cpu 表示只有一个“大脑”,在某一个时间点上实际只能处理一件事,但 cpu 的处理速度极快,多个线程频繁切换执行给人一种同时做的错觉,所以单核的 cpu 不能够做到真正的多线程并发,但会给人一种“多线程并发”的感觉。
对于多核 cpu 的电脑在同一时间点上,可以真正的有多个进程并发执行。
实现线程的方式
java 支持多线程机制,并且 java 已经将多线程实现了,我们只需要继承就行。
java 语言中实现线程有两种方式:
1、编写一个类,直接继承java.lang.Thread
重写 run 方法。
public class ThreadTest{
public static void main(String[] args){
//新建一个分支线程
MyThread myThread = new MyThread();
//启动线程:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。只要新的栈空间开出来start()方法就结束了。
myThread.start();
for(int i=0;i<1000;i++){
System.out.println("主线程-->"+i);
}
}
}
class MyThread extends Thread{
@Overried
public void run(){
//这段程序运行在分支线程中
for(int i=0;i<1000;i++){
System.out.println("分支线程-->"+i);
}
}
}
代码分析:
myThread.start();
的任务是开启一个新的栈空间,只要新的栈空间开出来 start() 方法就结束了。启动成功的线程会自动调用 run() 方法,并且 run() 方法在分支栈的栈底部(压栈),main() 方法在主栈的栈底部, run() 方法和 main() 方法是平级的。
2、编写一个类,实现java.lang.Runnable
接口,实现 run 方法
public class ThreadTest02{
public static void main(String[] args){
//创建一个可运行对象
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{
@Overried
public void run(){
//这段程序运行在分支线程中
for(int i=0;i<100;i++){
System.out.println("分支线程-->"+i);
}
}
我们通常使用第二种方式创建线程,实现 Runnable 接口的同时还可以继承其他的类;因为 java 只支持单继承,第一种方式继承了 Thread 之后无法继承其他的类。
采用匿名内部类方式创建线程:
public class ThreadTest03{
public static void main(String[] args){
//匿名内部类方式创建线程
Thread t = new Thread( new MyRunnable(){
@Overried
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)
}
}
}
线程的生命周期:新建状态、就绪状态、运行状态、阻塞状态、死亡状态
获取当前线程对象
1、当线程没有设置名字的时候,默认的名字的规律如下
Thread-0,Thread-1,Thread-2…
2、获取线程对象的名字
String name = 线程对象.getName();
3、修改线程对象的名字
线程对象.setName("线程名字");
4、获取当前线程对象
static Thread currentThread();
//返回值t就是当前线程
Thread t = Thread.currentThread();
测试:
public class ThreadTest{
public void doSome(){
/*此时this是指doSome(),不是指线程
this.getName();
super.getName();*/
String name = Thread.currentThread().getName();
System.out.println("--->"+name);
}
public static void main(String[] args){
ThreadTest tt = new ThreadTest();
tt.doSome();
//currentThread指当前线程对象,在main方法中所以当前线程就是主线程
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName()); //main
//创建线程对象
MyThread t = new MyThread();
//设置线程的名字
t.setName("t1");
//获取线程的名字
String tName = t.getName();
System.out.println(tName);
//启动线程
t.start();
}
}
//线程类
class MyThread extends Thread{
public void run(){
for(int i=0;i<100;i++){
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName()+"-->"+i);
/*
System.out.println(super.getName()+"-->"+i); //super指MyThread的父类Thread
System.out.println(this.getName()+"-->"+i); //this指MyThread
*/
}
}
}
为什么获取当前线程一定要使用 Thread currentThread = Thread.currentThread();
?
答: MyThread 类是线程类所以可以使用super.getName()和this.getName()
获取线程的名字;但是在 doSome() 方法中因为不是线程类就不能使用这两种方法获取线程的名字,必须使用 Thread currentThread = Thread.currentThread();
线程的static void sleep(long millis)
方法:
Thread.sleep(1000);
参数是毫秒,效果是间隔特定的时间去执行一段特定代码。
public static void main(String[] args){
//创建线程对象
Thread t = new MyThread();
t.start();
//调用sleep方法
try{
//问题:这行代码会让线程t进入休眠状态吗?
t.sleep(1000 * 5);
}catch(InterruptedException e){
e.printStackTrace();
}
//5秒之后才执行
System.out.println("hello world");
}
class MyThread extends Thread{
public void run(){
for(int i=0;i<100;i++){
System.out.println("分支线程-->"+i);
}
}
问题: t.sleep(1000 * 5);
这行代码会让线程t进入休眠状态吗?
答:在执行的时候还是会转换成Thread.sleep(1000 * 5);
,这行代码的作用是让当前线程进入休眠,也就是说main线程进入休眠。
终止线程的休眠:
让一个正在休眠(sleep)的线程醒来。
public static void main(String[] args){
Thread t = new Thread(new MyRunnable());
t.setName("t");
t.start();
//5秒后,t线程醒来
try{
Thread.sleep(1000 * 5);
}catch(InterruptedException e){
e.printStackTrace();
}
//终止t线程的睡眠
t.interrupt();
}
class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+"-->begin");
try{
//睡眠1年
Thread.sleep(1000 * 60 *60 * 24 * 365);
}catch(){
//打印异常信息
e.printSackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->end");
}
}
问题:为什么Thread.sleep(1000 * 60 *60 * 24 * 365);
只能用 try/catch 处理异常,而不能用 throws 呢?
答:MyRunnable 的实现接口(父类) Runnable 的 run() 方法中没有 throws 异常,子类不能抛出比父类更宽泛的异常,MyRunnable 重写的 run() 方法所以只能用 try/catch 处理异常。
结论:run() 方法当中的异常不能 throws ,只能用 try/catch 处理异常,因为 run() 方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。
t.interrupt();
执行时 MyRunnable 的 run() 方法的Thread.sleep()
方法会抛异常,进入 catch() 捕捉处理异常,即没有执行Thread.sleep()
方法从而终止 t 线程的睡眠。
终止线程的执行:
1、强行终止线程:
使用线程对象.stop();
来强行终止线程(杀死线程),这个方法以过时不建议使用。
缺点:容易丢失数据,这种方式是直接将线程杀死,线程中没有保存的数据将会丢失。
2、合理的终止一个线程:
public static void main(String[] args){
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.setName("t");
t.start();
//主线程睡5秒后将t线程终止
try{
Thread.sleep(5000);
}catch(InterruptedException e){
e.printStackTrace();
}
//终止线程,将run标记修改为false就可以终止线程
r.run=false;
}
class MyRunnable 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;
}
}
}
}
线程调度
1、抢占式调度模型:
那个线程的优先级比较高,抢到的 CPU 时间片的概率就高一些,java 采用的就是抢占式调度模型。
2、均分式调度模型:
平均分配 CPU 时间片,每个线程占有的 CPU 时间片的时间长度一样。(cpu的时间片多一些是指线程处于运行状态的时间多,而不是抢执行权的能力(谁先执行谁后执行))
MAX_PRIORITY 最高优先级10
MIN_PRIORITY 最低优先级1
NORM_PRIORITY 默认优先级5
实例方法:
void setPriority(int newPriority) 设置线程的优先级
int getPriority() 获取线程优先级
优先级比较高的获取CPU时间片可能会多一些(大概率)。
静态方法:
static void yield() 暂停当前正在执行的线程对象,并执行其他线程。该方法不是阻塞方法,而是让当前线程从“运行状态”回到“就绪状态”,就绪之后还会再次强cpu时间片。(用法:Thread.yield();)
注意:优先级高只是线程抢到的cpu时间片多(大概率情况下),并不表示该线程一定优先执行。
Thread t = new Thread(new MyRunnable());
//设置优先级
t.setPriority(10);
//设置线程名
t.setName("t");
//启动线程
t.start();
//输出线程的优先级
System.out.println(Thread.currentThread().getName()+"线程的优先级为-->"+Thread.currentThread().getPriority());
3、合并线程(把多线程变成单线程)
void join()
合并线程(实例方法)
class MyThread1 extends Thread{
public void doSome(){
MyThrea2 t = new MyThread2();
t.join();
//当前线程(线程类MyThread1的对象)进入阻塞状态,t线程(MyThread2的对象)执行,直到t线程结束当前线程才继续执行
}
}
public static void main(String[] args){
System.out.println("main begin");
Thread t = new Thread(new MyRunnable());
t.setName("t");
t.start();
//合并线程
try{
t.join();//线程t合并到当前线程中,当前线程受阻塞,线程t执行直到结束
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("main over");
}
class MyRunnable implements Runnable{
@Override
public void run(){
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
结果:main begin …… main over
线程安全
1、多线程并发环境下数据的安全问题
在开发中,我们的项目运行在服务器中,而服务器已经将线程的定义、线程对象的创建、线程的启动等实现了,不再需要我们编写。
重要的是,我们必须关注自己编写的程序和数据在多线程并发的环境下是否安全。
2、什么时候数据在多线程并发的环境下会存在安全问题?
三个条件:多线程并发、有共享数据、共享数据有修改的行为。
上图中,银行账户存款为一万,两个人同时取钱,因为账户余额更新延迟,第一个人取出一万后余额没有及时更新,第二个人也能取出一万。
3、解决线程安全问题
线程同步机制:线程排队执行,用排队执行解决线程安全问题。(线程排队执行使数据安全了,但会牺牲一部分效率)
线程异步机制:线程各自执行,其实就是多线程并发,效率较高。