目录
1、小知识点
- 进程:启动一个应用程序就是启动了一个进程
- 线程:线程是进程中的一个执行单元
- 进程和线程的关系?
答:进程相当于一家公司,线程就是这家公司的员工;进程之间的内存是独立的,不共享,比如酷狗和英雄联盟的内存是独立的;线程之间的内存是共享的,其中栈内存是线程独有的,但是方法区内存和堆内存是线程共享的,也就是一个线程一个栈;
扩展:栈内存存储局部变量,堆内存存储实例变量,方法区内存存储静态变量,由于栈是线程独有的,所以局部变量是安全的,由于方法区内存和堆内存是线程共享的,所以实例变量和静态变量是不安全的 - 最早的计算机支持多进程吗?
答:不支持,最多的计算机只支持单进程,CPU的使用率比较低 - 多进程并发处理,有什么用呢?
答:提供了CPU的使用率,对于单核的计算机来说,多进程并发并不是真正意义上的多进程,因为CPU同一时间内只能执行一个进程,只是当我们来回切换的时候CPU也能快速的进行进程间的切换 - 多线程并发的作用?
答:同时处理多个业务,提高处理速度,其实多线程并发也是假的,对于单核CPU来说,CPU在同一时间内只能做一件事情,并不会同时做多件事情,多个线程在抢夺CPU的执行权(CPU时间片),谁抢到CPU的执行权,谁就可以执行,然后多线程频繁切换执行,所以让我们感觉是多线程同时执行的 - 如果两台电脑的QQ需要通信,需要什么?
答:那我首先要知道你电脑的IP地址,这样才能找到你的电脑,IP地址是计算机在网络中的唯一标识,在同一个网络中,IP地址不能重复;然后我要知道你的端口,这样才能找到你的应用,一个应用启动的时候会开启一个进程,这个进程会拥有一个端口,端口是进程的唯一标识,在同一台计算机中,端口不能重复;
2、创建线程的3种方法
方法1、继承Thread,重写run()方法
代码:
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for (int i=0;i<1000;i++){
System.out.println("主线程-----------------"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int j=0;j<1000;j++){
System.out.println("分支线程》》》》》》》》》"+j);
}
}
}
解释:定义MyThread类,继承Thread类,使用Ctrl+O重写run()方法,在main()方法里面定义Thread类对象,其实MyThread实现了Thread之后已经成为了一个线程,所以可以直接调用线程的start()方法开启myThread 线程,然后线程会执行线程中的run()方法,这样就实现了多线程,如下图:
方法2、直接定义Thread对象,重写run()方法(同方法1,都是继承Thread类)
代码:
public class Test {
public static void main(String[] args) {
Thread thread = new Thread("branchThread"){
@Override
public void run() {
for (int j=0;j<1000;j++){
System.out.println("分支线程》》》》》》》》》"+j);
}
}
};
thread.start();
for (int i=0;i<1000;i++){
System.out.println("主线程-----------------"+i);
}
}
}
方法3、实现Runnable,实现run方法
代码:
public class Test {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
for (int i=0;i<1000;i++){
System.out.println("主线程-----------------"+i);
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int j=0;j<1000;j++){
System.out.println("分支线程》》》》》》》》》"+j);
}
}
}
解释:MyRunnable类实现了Runnable接口,然后MyRunnable变成了一个可执行的类,可不是线程类,然后使用Ctrl+O实现run()方法,Thread线程对象定义的时候,里面是一个可执行的类,所以可以定义MyRunnable类对象,然后在Thread定义的时候把这个类对象放入()里面,使用start()启动线程,线程调用可执行的类中的run()方法
注意:线程调用的是start()方法,目的是开启一个线程,然后让线程去调用run()方法,不能在main方法里面直接调用run()方法,那样就是普通的方法调用,而不是多线程,因为就没有执行start()方法,也就没有其他的线程,这两种方法来说,实现Runnable比较好,因为这种方法把继承让了出来,扩展性比较强
方法4、使用Callable接口
尚硅谷JUC笔记中第七集:Callable
3、线程生命周期
包括:新建状态(new)、就绪状态(runnable)、运行状态(running)、阻塞状态(blocked)、死亡状态(dead)
- JVM对线程的调度模型:
答:JVM采用抢占式调度模型,优先级较高的,可能获得的CPU时间片较多一点 - 获取当前线程、设置当前线程名称、获取当前线程名称的几个方法:
// 获取当前线程,注意这个是静态方法,无论是谁调用它,获取的都是d当前方法所在的线程
Thread thread = Thread.currentThread();
// 设置当前线程名称
thread.setName("主线程");
// 获取当前线程名称
String name =thread.getName();
4、线程中的sleep方法
例1:
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
// 由于父类中的run()方法都没有抛出异常,所以子类重写的run()方法也不能抛出异常,这也就是为什么线程睡眠不能向上抛出异常的原因
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
结果:
执行1次,睡眠1秒
例2:
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
try {
myThread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello world!");
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
结果:
分支线程执行完毕后,等待了不到1s,输出了hello world!,可能你就有疑虑了,为什么我是使用分支线程调用的sleep方法,为什么受影响的倒是主线程,这是因为sleep()方法是一个静态方法,无论哪个名称调用它,其实编译的时候都编译成了Thread.sleep(1000);,所以这相当于是主线程睡眠了1s,简单来说就是由于sleep()方法是静态方法,所以在哪个线程中调用sleep()方法,就会让哪个线程睡眠,无论是使用什么调的sleep()方法
拓展:
其实上面的静态方法调用的问题,我还可以在举一个例子:
public class Test03 {
public static void main(String[] args) {
MyClass myClass = new MyClass();
MyClass.myFangFa();
myClass = null;
myClass.myFangFa();// 静态方法和.前面的东西没有半毛钱关系,其实真正编译之后还是MyClass.myFangFa();
}
}
class MyClass{
public static void myFangFa(){
System.out.println("方法执行");
}
}
结果:
方法执行
方法执行
解释:
可能你会认为会产生空指针异常,但是不会的,因为myFangFa是静态方法,所以无论谁调用它,编译的时候都会变成类调用它,所以即使前面调用的值为空,都是没有任何问题的,因为静态方法都是被静态方法所属的类直接调用的
5、如何中断一个线程的睡眠?
答:使用interrupt()方法,这种方法可以让处于休眠状态的线程抛出异常,从而中断睡眠,例子如下:
public class Test05 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
thread.interrupt();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000*60*60*24*365);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("起来干活了");
}
}
结果:
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.bjpowernode.test.MyRunnable.run(Test05.java:20)
at java.lang.Thread.run(Thread.java:745)
起来干活了
6、如何强制终止线程的执行?
答:使用stop()方法,可以强行终止线程的执行,例子如下:
public class Test05 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i =0;i<10;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
});
thread.setName("分支线程");
thread.start();
for (int i = 0;i<5;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
thread.stop();
}
}
结果:
分支线程--->0
分支线程--->1
分支线程--->2
分支线程--->3
分支线程--->4
7、如何合理终止线程的执行?
答:在分支线程里面设置一个布尔标记值,外部可以对布尔标记值进行修改,布尔标记值默认是true,把布尔标记值放在if里面,下面设置一个else,else里面设置break;,然而if函数呢放在一个while循环里面,当我不想在执行线程中循环的时候,通过外部把布尔标记值变成false,然后执行else里面的break,退出循环就可以了,例子如下:
public class Test05 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
for (int i=0;i<5;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
myRunnable.setRun(false);
}
}
class MyRunnable implements Runnable{
private boolean isRun = true;
public void setRun(boolean isRun){
this.isRun = isRun;
}
int i=0;
@Override
public void run() {
while (true){
if (isRun){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->"+i++);
}else {
break;
}
}
}
}
结果:
Thread-0--->0
Thread-0--->1
Thread-0--->2
Thread-0--->3
Thread-0--->4
8、线程优先级
代码:
public class Test {
public static void main(String[] args) {
System.out.println("线程最大优先级:"+Thread.MAX_PRIORITY);
System.out.println("线程最小优先级:"+Thread.MIN_PRIORITY);
System.out.println("线程正常优先级:"+Thread.NORM_PRIORITY);
System.out.println("线程默认优先级:"+Thread.currentThread().getPriority());
// 设置当前线程优先级是1,这两个方法属于实例方法
Thread.currentThread().setPriority(1);
}
}
结果:
线程最大优先级:10
线程最小优先级:1
线程正常优先级:5
线程默认优先级:5
总结:
线程优先级较高的,获取的CPU时间片就多一点,不过优先级较低的线程也可以抢夺CPU时间片,不过抢到的比较少而已
9、线程让位
使用yield()方法进行让位,让位的意思就是在这个方法执行之后短时间不去抢夺CPU执行权,如果已经抢到CPU执行权,那就放弃CPU执行权,这个方法是一个静态方法,在哪一个线程里面就让哪一个线程让位,线程让位之后可以重新抢夺CPU执行权,让位的时间是很短暂的,例子如下:
public class Test {
public static void main(String[] args) {
MyRunnable1 m1 = new MyRunnable1();
MyRunnable2 m2 =new MyRunnable2();
Thread thread1 = new Thread(m1);
Thread thread2 = new Thread(m2);
thread1.setName("m1");
thread2.setName("m2");
thread1.start();
thread2.start();
}
}
class MyRunnable1 implements Runnable{
@Override
public void run() {
for (int i =0;i<10000;i++){
if (i%100==0){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"》》》》》》》》》》"+i);
}
}
}
class MyRunnable2 implements Runnable{
@Override
public void run() {
for (int i =0;i<10000;i++){
System.out.println(Thread.currentThread().getName()+"=================="+i);
}
}
}
结果:
当执行第700次的时候发现取余100是0,所以线程让位,这个时候m2线程执行
10、线程合并
使用的是join()方法,如果把其他的线程合并到某个线程上,那么就先执行合并过来的其他线程,当其他线程执行完毕之后,在执行本线程,代码如下:
public class Test {
public static void main(String[] args) {
MyRunnable1 m1 = new MyRunnable1();
Thread thread1 = new Thread(m1);
thread1.setName("m1");
thread1.start();
for (int i =0;i<100;i++){
if (i==50){
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"================="+i);
}
}
}
class MyRunnable1 implements Runnable{
@Override
public void run() {
for (int i =0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"》》》》》》》》》》"+i);
}
}
}
结果:
解释:
可以看到,当i=50的时候,main线程停止运行,m1线程开始运行,当m1线程执行完毕,main()线程继续运行
11、线程同步机制概述
1、什么条件下需要考虑线程的安全性呢?
答:一、多线程并发环境下;二、多线程共享数据;三、所共享的数据涉及到修改的操作;
2、在多线程并发的环境下,如何保证数据的安全性?
答:使用线程同步机制,也就是说程序需要符合同步编程模型
3、什么是线程同步机制?
答:线程之间出现了等待关系,线程排队执行,这就是线程同步机制
4、什么是同步编程模型,什么是异步编程模型?
答:线程之间出现了等待关系,线程排队执行,这就是同步编程模型;线程之间没有出现等待关系,多线程同时并发执行,这就是异步编程模型;简单来说同步就是线程等待线程,异步就是多线程并发执行
- 实现线程同步机制
1、为什么要考虑线程同步机制呢?
假设有这么一个例子,夫妇二人分别使用银行卡和存折去对一张卡取钱,如果丈夫取完了钱,发生了网络延迟,然后妻子同时也在取钱,这个时候妻子也在取钱,如果两个线程不是同步的,那么妻子也可以取钱,当妻子取完了钱,更新了账户余额,丈夫那边的网络延迟解除了,这个时候也会更新账户余额,那么这个时候银行就亏了,所以说需要线程同步机制,对于上面的例子我使用两个线程去实现,使用sleep()方法去完成网络延迟的功能,代码如下:
public class Test {
public static void main(String[] args) {
Account account = new Account();
account.setActno("银行卡");
account.setBlance(2500);
MyRunnable m1 = new MyRunnable(account);
MyRunnable m2 = new MyRunnable(account);
Thread thread1 = new Thread(m1);
Thread thread2 = new Thread(m2);
thread1.setName("m1");
thread2.setName("m2");
thread1.start();
thread2.start();
}
}
class MyRunnable implements Runnable{
private Account account;
public MyRunnable() {
}
public MyRunnable(Account account) {
this.account = account;
}
@Override
public void run() {
double money = 1000;
account.withdraw(money);
System.out.println(Thread.currentThread().getName()+"取款"+money+"元,取后剩余"+account.getBlance()+"元");
}
}
class Account{
private String actno;
private double blance;
public Account() {
}
public Account(String actno, double blance) {
this.actno = actno;
this.blance = blance;
}
public String getActno() {
return actno;
}
public Account setActno(String actno) {
this.actno = actno;
return this;
}
public double getBlance() {
return blance;
}
public Account setBlance(double blance) {
this.blance = blance;
return this;
}
/**
* 取出钱,然后更新余额
* @param money 取出的钱
*/
public void withdraw(double money){
double afterBlance = this.blance-money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBlance(afterBlance);
}
}
结果:
m2取款1000.0元,取后剩余1500.0元
m1取款1000.0元,取后剩余1500.0元
现在使用同步代码块去修改上面的代码:那就是withdraw()方法中的代码使用synchronized(){}括起来,其他的程序不变,如下面的代码:
public void withdraw(double money){
synchronized (this){
double afterBlance = this.blance-money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBlance(afterBlance);
}
}
如果在执行,结果如下:
m1取款1000.0元,取后剩余1500.0元
m2取款1000.0元,取后剩余500.0元
所以说通过synchronized 就实现了线程同步机制,括号里面的是共享对象,注意要是引用数据类型,只要这个对象是线程共享的对象,代码中的this指代的是账户对象,这个账户对象是两个线程共享的,或者可以换成actno也可以,actno是实例变量,存储在堆中,也是两个线程共享的,即使是在账户类中随便定义一个实例变量,也是可以的,因为实例变量存储在堆中,所以是两个线程共享的,也就是说()里面的对象是两个线程共享的就可以了,但是blance是不可以的,因为这个不是引用数据类型,如果你在withdraw()方法中定义一个String类型的变量也是不可以的,因为局部变量在栈中存储,由于一个线程一个栈,局部变量不是线程共享的,所以局部变量不能作为共享对象;
synchronized 不仅可以这样使用,还可以把synchronized 放在实例方法里面,例如可以把withdraw()方法更改一下,代码如下:
public synchronized void withdraw(double money){
double afterBlance = this.blance-money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBlance(afterBlance);
}
这样产生的结果也是一样的,都是实现了线程同步机制,但是这个共享对象已经被确定了,就是this,this指代谁调用这个方法,那么this就是谁,那么共享对象就是谁,但是这个有一个弊端就是有的时候共享对象不是this,就不能使用这种方法了,并且同步代码块的范围是方法体中全部的代码,这样可能会拖慢程序执行的效率,其实同步代码块应该是在保证数据安全的前提下,尽可能的少,这样效率就会高
至于synchronized同步代码块的执行原理:假设m1线程和m2线程同时并发执行,当m1线程遇到了synchronized关键字,就会获取共享对象的对象锁,其实对象锁就是一个方便理解的名字,其实底层并不是一把锁,而是一个类似于0和1的标记,当m1线程获取到对象锁之后,就会把对象锁锁住,然后执行同步代码块里面的代码,当m1线程把里面的代码执行完了,然后就会打开对象锁,如果m1线程还没有把同步代码块里面的代码执行完,m2也遇到了synchronized关键字,它也会获取共享对象的对象锁,但是发现对象锁被锁住了,m2线程就会等待对象锁打开才会执行同步代码块的代码
说起来对象锁了,还有一个是类锁,就是把synchronized放在静态方法上,因为对象可以有多个,但是类只有一个,所以对象锁可以有多把,但是类锁只有一把,只要锁了其他的所有线程都不能使用了,例子代码如下:
public static synchronized void hello(){
System.out.println("hello");
}
12、线程同步的几个经典面试题
面试题1:
public class Test01 {
public static void main(String[] args) {
MyClass myClass = new MyClass();
MyRunnable myRunnable1 = new MyRunnable(myClass);
MyRunnable myRunnable2 = new MyRunnable(myClass);
Thread t1 = new Thread(myRunnable1);
Thread t2 = new Thread(myRunnable2);
t1.setName("m1");
t2.setName("m2");
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable{
private MyClass myClass;
public MyRunnable(MyClass myClass) {
this.myClass = myClass;
}
@Override
public void run() {
if ("m1".equals(Thread.currentThread().getName())){
myClass.m1();
} else {
myClass.m2();
}
}
}
class MyClass{
public synchronized void m1(){
System.out.println("m1 begin");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m1 end");
}
public void m2(){
System.out.println("m2 begin");
System.out.println("m2 end");
}
}
结果:
m1 begin
m1 end
m2 begin
m2 end
解释:
如果m1线程先获得CPU执行权,那就会执行m1()方法,但是m1()方法上面有synchronized关键字,所以执行到m1()方法的时候,会去锁池中找对象锁,然后遇到了sleep()方法,就进入了阻塞状态,这个时候m2线程就有机会去获得CPU执行权了,因为m2()方法上面没有synchronized关键字,所以不会去找对象锁,直接执行m2()中的代码;
面试题2:
把m2()方法上面也加上synchronized关键字,其他的不变
解释:
这个时候如果执行m1()方法,然后睡眠,在执行m2()方法,当看到synchronized关键字的时候,就会去找对象锁,这个时候发现锁被m1()方法占用了,这个时候m2()方法只有等待m1()方法执行完毕了,才会执行
面试题3:
在面试题2的基础上,给m1和m2方法加一个关键字static
解释:
这个考察的是类锁,无论一个类的对象锁有多少把,但是一个类只有一个类锁,静态方法是类的方法,假设m1线程先执行,把类锁占用了,然后睡眠,当m2线程获得CPU时间片的时候,也会去找类锁,发现类锁被m1线程占用了,所以只有等待m1()方法执行完毕,才能执行m2线程中的m2()方法
13、死锁
代码:
public class Test {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
MyThread2 myThread2 = new MyThread2();
Object o1 = 1;
Object o2 = 2;
myThread1.setObject(o1, o2);
myThread2.setObject(o1, o2);
myThread1.start();
myThread2.start();
}
}
class MyThread1 extends Thread{
private Object o1;
private Object o2;
public void setObject(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 MyThread2 extends Thread{
private Object o1;
private Object o2;
public void setObject(Object o1,Object o2){
this.o1=o1;
this.o2=o2;
}
@Override
public void run() {
synchronized (o2){
// 使用sleep(),让死锁一定发生
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
解释:
同步代码块嵌套,很容易造成死锁,死锁在实际开发中不容易排查,所以尽可能减少同步代码块嵌套的使用
- 守护线程
public class Test {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
myThread1.setName("守护线程");
// 设置成主线程的守护线程
myThread1.setDaemon(true);
myThread1.start();
for(int i = 1;i<=5;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"====执行了"+i+"秒");
}
}
}
class MyThread1 extends Thread{
@Override
public void run() {
int i = 1;
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"》》》执行了"+(i++)+"秒");
}
}
}
结果:
守护线程》》》执行了1秒
main====执行了1秒
main====执行了2秒
守护线程》》》执行了2秒
main====执行了3秒
守护线程》》》执行了3秒
main====执行了4秒
守护线程》》》执行了4秒
main====执行了5秒
守护线程》》》执行了5秒
解释:
只要把setDaemon()里面的参数设置成true,那就是把这个线程设置成守护线程了,守护的是你设置setDaemon()的代码所在的那个线程,比如这个就是分支线程守护主线程,守护线程一般都是死循环,但是当用户线程结束的时候,守护线程会自动结束
14、定时器
代码:
public class Test {
public static void main(String[] args) {
Timer timer = new Timer();
Date date = null;
try {
date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2019-12-8 12:35:00");
} catch (ParseException e) {
e.printStackTrace();
}
// 第一种方法,从data开始,经过10s执行一次任务
timer.schedule(new Task(),date,1000*10);
// 第二种方法
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("主任务执行了");
}
}, date, 1000*5);
}
}
class Task extends TimerTask {
@Override
public void run() {
System.out.println("任务执行了");
}
}
结果:
任务执行了
主任务执行了