1.什么是线程?
在Java编程思想第四版中是这样定义的:利用对象,可将一个程序封装为互相独立的区域,我们通常也需要将一个程序转化为多个独立运行的子任务。这样的每个子任务叫做一个“线程”。在编写程序时,可将每个线程都想像为独立运行,而且都有自己的CPU。
2.什么是进程?
进程是指一种“自包容”运行程序,有自己的地址空间,“多任务”操作能同时运行多个进程(程序),但实际由于CPU分时机制的作用,使每个CPU都能循环获取自己的CPU时间片。由于轮换速度非常快,使所有的程序像是在同时进行一样。线程是进程内部单一的一个顺序控制流,因此,一个进程可能容纳了多个同时执行的线程。
3.实现线程的两种方式
3.1 继承Thread类
java有多个线程但是只有一种Thread类
Thread是个表示线程的类,它有着启动线程,连接线程,让线程闲置等方法…
当有超过一个以上的执行空间时,看起来像是有好几件事在同时发生,只有多处理器系统才能做到,使用java可以让他看起来像是同时在执行,但是实际上只是在执行空间中快速的切换,因此才会感觉每项任务都在同时执行。
TestThred 类继承了Thread
public class TestThred extends Thread{
@Override
public void run() {
//一般一个线程的任务就会写在这里面
for (int i =0;i<5;i++){
System.out.println("业务逻辑:"+i);
}
}
}
public class Test {
public static void main(String[] args){
Thread t1 = new TestThred();
System.out.println("开启多线程咯~");
t1.start();
System.out.println("=============1===================");
System.out.println("=============2===================");
System.out.println("=============3===================");
}
}
在以上代码中:
1.Thread t1 = new TestThred();
表示创建了Thread实例,但是还没启动,也就是说,有Thread对象,没有执行中的线程
2.t1.start()
:表示变成了可执行状态,只要轮到她了就可以执行了,该线程已经布置好了空间
3.之后就靠java虚拟机线程调度机制来决定了
一旦线程进入了可执行状态,他会在可执行和执行中两种状态中来来去去,同时也有另外一种状态:暂时不可执行,也叫被堵塞状态
以下是上面代码的几次运行结果:
从运行结果可以看出,main方法中的打印与run()方法的打印语句的执行顺序是不固定的,那么从这里就可以看出:
当main执行start()之后,mian方法的其它代码就与run()里的代码并行了,就像是一条河流的两个分支那样并行前进。各走各的,互不影响。这其实是由于线程调度器的作用。
3.2 实现Runnable接口
测试代码:
public class TestRunnable implements Runnable {
@Override
public void run() {
//Thread.currentThread().getName()是获取到线程的名字,可有可无
System.out.println(Thread.currentThread().getName()+"代码逻辑");
for (int i =0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"线程的业务逻辑:"+i);
}
}
}
public class Test {
public static void main(String[] args){
Thread t1 = new Thread(new TestRunnable(),"线程一");
t1.start();
System.out.println("=============1===================");
System.out.println("=============2===================");
System.out.println("=============3===================");
}
}
和继承了Thread的执行效果是一样的,每次保证自己线程内的执行顺序一样,新开线程相对于main()方法的执行是异步的,互不影响。
3.3 线程调度器
线程调度器会决定哪个线程从等待的状态中挑出来运行以及何时送回等待状态。他会决定哪个线程要运行多久,当线程被踢出时,调度器也会指定线程要回去等待下一次机会或者是堵塞。并且没有API可以控制调度器,调度无法确定,可以用setPriority()来设置线程的优先级,但是只是提高概率,不能百分百改变!但是有sleep的存在,可以让指定的线程“睡”指定的时间,在“睡觉”时间内线程肯定是不会运行的,其它的就无法保证。
3.4继承方式和实现方式的区别
3.4.1 区别
继承Thread:线程代码存放于Thread子类的run方法中(重写)
实现Runnable:线程代码存放于接口子类的run方法中(实现)
3.4.2 实现方法的好处
两种方法一般使用实现的方法来实现多线程:
1.避免了单继承的局限性
2.多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源
例:
public class TestRunnable implements Runnable {
//被两个线程共享的资源
int count = 0;
@Override
public void run() {
//Thread.currentThread().getName()是获取到线程的名字,可有可无
System.out.println(Thread.currentThread().getName()+"代码逻辑");
for (int i =0;i<5;i++){
count ++;
System.out.println(Thread.currentThread().getName()+"线程的业务逻辑:"+count);
}
}
}
测试代码:
public class Test {
public static void main(String[] args){
Runnable runnable = new TestRunnable();
Thread t1 = new Thread(runnable,"线程一");
t1.start();
Thread t2 = new Thread(runnable,"线程二");
t2.start();
System.out.println("=============1===================");
System.out.println("=============2===================");
System.out.println("=============3===================");
}
}
运行结果:
4 线程的生命周期
截图来源:求知课堂
线程的完整生命周期经历了五个状态:
1.新建:当一个Thread类或其子类的对象被声明并创建的时候,新的线程对象处于新建状态
2.就绪:处于新建状态的线程被start()之后,进入线程队列等待CPU时间片,此时具备了运行条件
3.运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态,run()方法定义了线程的操作和功能
4.阻塞:在某种特殊情况下,被认为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态
5.死亡:线程完成了他的全部工作或者被强行终止
5 线程的同步和死锁
我们先来分析一下这个场景:
小王和小红对同一个账户(内有3000元)同时进行取钱操作,各取2000元,这样操作是有可能会出现负数钱的:
public class TestSynchronized {
public static void main(String[] args){
Acount acount = new Acount();
//多线程对象
User u_xiaohong =new User(acount,2000);
User u_xiaowang =new User(acount,2000);
Thread xiaohong = new Thread(u_xiaohong,"小红");
Thread xiaowang = new Thread(u_xiaowang,"小王");
xiaohong.start();
xiaowang.start();
}
}
/**
* 进行取钱操作
*/
class Acount{
public static int money = 3000;
public void drawing(int m){
//操作者的名字
String name = Thread.currentThread().getName();
//取钱之前加一个判断,如果钱不够的话不给取
if (money<m){
System.out.println(name+"操作,余额不足~");
}else {
System.out.println(name + "操作,原有账号有" + money + "块钱~");
System.out.println(name + "操作,取款金额" + m);
money = money - m;
System.out.println(name + "操作,余额" + money);
}
}
}
/**
* 用户操作
*/
class User implements Runnable{
Acount acount;
int money;
public User(Acount acount,int money){
this.acount = acount;
this.money = money;
}
@Override
public void run() {
//取钱
acount.drawing(money);
}
}
运行结果:
我们可以看到,就算是在取钱之前加了一个判断,但是使用多线程的方法去取钱的话,判断就起不了作用,因为线程在共享资源时,一个线程还没执行完毕的时候,另一个线程就开始执行,会导致读取到的共享资源有问题。
解决方法:
对于多条操作共享数据语句时,只能让一个线程都执行完,在执行过程中,其它线程都不可以执行。
即在取钱操作的时候加上同步锁:可以直接在方法上面加
运行结果:
注意:加普通方法上加synchronized操作的是对象,而不是普通方法。
参考书籍资料:《Java编程思想》 第四版;《Head First Java》;求知课堂网络视频