java语言之所以有多线程,目的是提高程序的运行效率。
进程是一个应用程序(一个进程是一个软件)。
线程是一个进程中的执行场景、执行单元。一个进程可以启动多个线程
对于java程序来说,当在DOS命令窗口中输入:
java HelloWorld 回车之后。会先启动JVM,而JVM就是一个进程。
JVM再启动一个主线程调用主方法(main),同时在启动一个垃圾回收的线程负责看护,回收垃圾。
所以,目前java程序至少有2个线程并发,一个是执行main方法的主线程。一个是垃圾回收线程。
两个进程内存独立不会共享,同一个进程的两个线程堆内存和方法区共享,但是栈内存独立,一个线程一个栈。
假设有10个线程就有十个栈。每个栈直接互不干扰,各自执行,这就是多线程并发。
主线程结束了,子线程还会执行吗?
Thread t1=new Thread(new Runnable() {
public void run() {
for (int i = 0; i <= 100; i++) {
if(i==100) {
System.out.println("子线程执行结束了!");
}
}
}
});
t1.start();
System.out.println("主线程执行结束了!");
通过测试可以看出主线程结束了,子线程仍然还在运行
有没有函数可以让主线程等子线程结束了才结束运行主线程
有的。
通过在主线程的任务中用子线程调用join()就可。
join解释成:等待线程死亡。
public static void main(String[] args) throws InterruptedException {
test t2=new test();
t2.start();
t2.join();
System.out.println("主线程执行");
}
}
class test extends Thread{
public void run() {
System.out.println("子线程执行");
}
}
输出结果:
子线程执行
主线程执行
什么是真正的多线程并发?对于多核cpu电脑真正的多线程并发肯定没有问题。
t1线程执行t1的,t2线程执行t2的,t1不会影响t2的,t2也不会影响t1的,这就做真正的多线程并发。
单核的cup只有一个‘大脑’,不能做到真正的多线程并发,但是可以给人一种多线程并发的感觉。
对于单核的cup来说,在某个时间点上实际只能处理一件事情,由于cup速度极快,多个线程频繁切换,给人造成多
线程并发的错觉。
举个例子:
网上最近很火的小书,通过大拇指的松动给人一种画面在动的错觉。
还有很著名的”膝跳反应原理“,拿小锤在你膝盖上敲,到感到痛觉,对于人的反应来说已经很快了,觉得就是同步的,其实不是,它会有个反应时间,这个反应时间对于我们人来说感觉不到,但是对于计算机来说,可以进行数亿次运算了,人类的大脑就好比单核计算机的cpu。
直接通过子类去继承父类(Thread),重写里面的run()方法
public static void main(String[] args) {
//main方法在主线程中,在主栈运行
//创建一个线程的对象
MyThread myThread=new MyThread();
//启动线程
myThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程----->"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
// 编写程序,运行在分支线程(栈)中
for (int i = 0; i < 10; i++) {
System.out.println("分支线程---->"+i);
}
}
}
start()方法的作用是:在JVM中开辟一块新的栈空间,开启后start()方法瞬间结束。
只要空间开出来,线程就启动成功了,分支线程自动调用run()方法执行程序,并且start()方法会在分支栈的最底部,和主线程的main方法差不多。
下面代码有什么区别?
myThread.run();
myThread.start();
直接调用run方法,分支栈并没有开辟出来,所以还是单线程。
而调用start方法,分支栈开辟出来,启动了多线程。
Runnable并不是一个线程类,而是一个可运行的类,在主线程中创建一个可运行的对象,将可运行的对象封装成一个线程对象。
public static void main(String[] args) {
MyRunnable myRunnable=new MyRunnable();
Thread thread=new Thread(myRunnable);
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程----->"+i);
}
}
}
class MyRunnable implements Runnable{
public void run() {
// 编写程序,运行在分支线程(栈)中
for (int i = 0; i < 10; i++) {
System.out.println("分支线程---->"+i);
}
}
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
public void run() {
System.out.println("支线程运行");
}
});
t.start();
System.out.println("主线程执行");
}
众所周知,前面两种线程不会有返回值,还有另一种线程的实现方法可以有返回值
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 创建未来任务对象
FutureTask <Integer>future=new FutureTask<Integer>(new Callable<Integer>() {
public Integer call() throws Exception {
int a=10;
int b=20;
return a+b;
}
});
Thread t1=new Thread(future);
t1.start();
System.out.println(future.get());
}
小结:这种线程实现的方式可以获取到线程结束后返回的值,call方法类似于run方法,不同的是:call方法会有返回值。
这让我想起来前一段时间的一道考试题,这道题我就卡在了不能获取线程的返回结果,学习到这,我重新想到那题的解决办法:
问题是:创建2个线程,一个线程求100内的偶数,一个线程求100内余数,并求和。
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask<Integer> future=new FutureTask<Integer>(new Callable<Integer>() {
public Integer call() throws Exception {
int num=0;
for (int i = 1; i <=100; i++) {
if(i%2==0) {
num+=i;
}
}
return num;
}
});
FutureTask<Integer> future2=new FutureTask<Integer>(new Callable<Integer>() {
public Integer call() throws Exception {
int num2=0;
for (int i = 1; i <=100; i++) {
if(i%2!=0) {
num2+=i;
}
}
return num2;
}
});
Thread t1=new Thread(future);
Thread t2=new Thread(future2);
t1.start();
t2.start();
System.out.println(future.get()+future2.get());
小结:这种方法优点是:可以获取到线程的返回值。缺点是,效率低,在获取线程返回值的时候,当前线程会阻塞。
线程有默认的名字 ---->Thread-0
可以通过setName方法进行修改线程名字。
获取到当前线程对象
Thread t=Thread.currentThread();
返回值t就是当前线程。如果出现在主线程中当前线程就是主线程。
新建状态是新new出来的线程对象。
就绪状态又叫可运行状态,表示当前的线程具有抢夺cpu时间片的权力(CPU时间片就是执行权)。
当一个线程抢夺到cpu时间片之后,就会开始执行run方法,run方法执行代表着线程进入运行状态。
run方法开始执行标志着这个线程进入运行状态,当之前占有的cpu时间片用完之后,会重新回到就绪状态继续抢夺cpu时间片,当再次抢到cpu时间片之后,会重新进入run方法接着上一次的代码继续往下执行。
当run方法执行完,线程死亡。
当一个线程进入到阻塞事件,例如接受用户的键盘输入,或者sleep方法等,此时线程会进入阻塞状态,阻塞状态的线程会放弃之前占有的
cpu时间片。重新进入到就绪状态继续抢cpu时间片。
static void sleep(long millis);
sleep是一个静态的方法,参数是毫秒,作用是让当前线程进入休眠,进入’阻塞状态‘,放弃占有的cpu时间片,让给其他线程使用。
public static void main(String[] args) {
System.out.println("主线程执行");
try {
Thread.sleep(5000);
} catch (Exception e) {
e.getStackTrace();
}
System.out.println("线程休眠5秒后输出了");
}
5秒后:
面试题:判断下面代码运行效果
public static void main(String[] args) {
Thread t=new MyThread2();
t.setName("t");
t.start();
try {
//t线程会休眠吗??
t.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("主线程");
}
}
class MyThread2 extends Thread{
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("当前线程"+Thread.currentThread().getName()+"\t"+i);
}
}
}
分析:t.sleep()出现在主线程中,虽然它是由线程对象点出来的,但是它是静态方法和引用没有任何关系,出现在哪个地方,就会让哪个线程进入休眠,所有说这个程序会先执行分线程,5秒后输出”主线程“。
interrupt方法叫”干扰“,会中断线程的睡眠,靠的是java的异常处理机制。
public static void main(String[] args) {
Thread t=new MyThread2();
//分支线程开启
t.start();
try {
Thread.sleep(2000);//主线程休息2秒
} catch (Exception e) {
e.printStackTrace();
}
t.interrupt();//让分支线程型“醒过来”
}
}
class MyThread2 extends Thread{
public void run() {
try {
Thread.sleep(20000000);//线程休眠很长时间
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("分支线程");
}
}
理解:当开始运行run方法的时候,线程会进入长时间休眠状态,通过执行interrupt方法会让分支线程出异常,直接执行catch语句。
public static void main(String[] args) {
Thread t=new MyThread2();
t.start();
//主线程休息5秒钟直接后干死分支线程
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
t.stop();//强行中断线程
}
}
class MyThread2 extends Thread{
public void run() {
//这个程序会运行10秒钟
for (int j = 0; j <10; j++) {
System.out.println(Thread.currentThread().getName()+j);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
stop的缺点:容易丢失数据,线程没有保存的数据会丢失,所以过时了。
抢占式:哪个线程的优先级比较高,抢到的cpu时间片的概率就多一些。java采用的就是抢占式调度模型。
均分式:平均分配cpu时间片,每个线程占有的cpu时间片时间长度一样,平均分配
实例方法:
void setpriority(int newPriority)//设置线程优先级
int getPriority()//获取线程优先级
静态方法:
static void yield()//暂停当前正在执行的线程,并执行其他线程
yield方法不是阻塞,会让当前线程”运行状态“回到”就绪状态“。
最低优先级是1,默认是5,最高是10.
需要满足三个条件
1.多线程并发环境下。
2.有共享数据。
3.共享的数据有修改行为。
线程排队执行(不能并发),用排队执行解决线程安全问题,会牺牲一部分效率。
这种机制被成为:线程同步机制。
异步编程模型:线程t1和t2,各自执行,就做异步编程模型。就是多线程并发。
同步线程模型:线程t1执行的时候,必须等待线程2执行结束,两个线程之间出现了等待关系,这就是同步线程模型。
怎么实现同步机制呢,synchronized代码块
每一个堆中的对象都有一把锁,这把锁只是一个标记。
假设t1和t2线程并发,线程1先执行了,遇到了synchronized,这个时候自动找后面线程共享对象的对象锁,找到之后并占有一把锁,然后执行
同步代码块中的程序,在执行过程中会、一直占有这把锁,直到同步代码块结束,这把锁才会释放。
总而言之,一定时间只有一个线程会执行程序,下个线程在同步代码块外等待。
注意事项:需要同步线程一定要有个共享对象。
在java中有三大变量:
实例变量:在堆中
静态变量:在方法区
局部变量:在栈中
在这三者中只有局部变量永远不会有线程安全问题。原因在于局部变量在栈中,永远都不会共享。
解决方法:
1.使用局部变量代替实例和静态变量
2.如果用实例变量,多new对象。
3.迫不得已用synchronized,用线程同步机制。
在Java中,有两大类线程:
第一种是用户线程,第二种是守护线程(也称作后台线程)。
守护线程具有代表是java中的垃圾回收线程。
主线程是一个用户线程。
守护线程的特点:
1.一般守护线程都是死循环。
2.用户线程结束,守护线程自动自动结束。
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new Runnable() {
//死循环线程
public void run() {
int i=0;
while(true) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+(i++));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t1.setName("分支线程");
t1.setDaemon(true);
t1.start();
//主线程执行代码
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+i);
Thread.sleep(1000);
}
}
}
执行结果:在主线程输出完之后,守护线程死循环也跟着结束了。
定时器有什么用呢?
目的是控制程序根据你设定的时间间隔去执行程序。
public static void main(String[] args) throws ParseException {
Timer timer=new Timer();
//日期类
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//获取当前时间
Date firstTime= sdf.parse("2021-4-22 15:10:00");
timer.schedule(new LogTimerTask(),firstTime,1000);
}
}
//定时任务类
class LogTimerTask extends TimerTask{
public void run() {
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time=sdf.format(new Date());
System.out.println(time+"完成任务!");
}
}
小结:计时器可采用线程守护的方式,可以用上方法,还能用匿名内部类形式。
Object o=new Object();
o.wait();
wait方法的作用是,让o对象中正在运行的线程进入等待状态并释放掉o对象的锁,无限期等待,直到线程被再次唤醒。
Object o=new Object();
o.notify();
notify的方法的作用是:让o对象进入等待状态的线程“苏醒”过来继续执行。
还有一个,notifyAll方法是让所有o对象的线程”苏醒“过来。
notify和wait方法都是在synchronized基础之上进行的。
交替打印奇数偶数
public static void main(String[] args) {
Num num=new Num();
Thread t1=new Thread(new Os(num));
Thread t2=new Thread(new Js(num));
t1.start();
t2.start();
}
}
class Num{
int num=1;
public int getNum() {
return num;
}
public int printNum() {
return num++;
}
}
class Os implements Runnable{
Num num;
public Os(Num num) {
this.num=num;
}
public void run() {
while(num.getNum()<100) {
synchronized (num) {
if(num.getNum()%2==0) {
try {
num.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+num.printNum());
num.notify();
}
}
}
}
class Js implements Runnable{
Num num;
public Js(Num num) {
this.num=num;
}
public void run() {
while(num.getNum()<100) {
synchronized (num) {
if(num.getNum()%2!=0) {
try {
num.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"\t"+num.printNum());
num.notify();
}
}
}
}
小结:01线程输出奇数,02线程输出.当数字1进入到Thread-0线程,synchrozed会锁住Num对象,进行判断为false,输出1,唤醒等待的thread-1线程执行。