博主声明:
转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。
-
进程、线程介绍
多线程编程是我们图形化操作系统的基本要求,比如之前的DOS操作系统,它以命令行的形式来获取用户行为,这种方式比较单一,程序在同一时间内也不会去做其他工作。再比如现在的Windows操作系统、Linux系统也罢,只要是提供丰富的图形化界面的操作系统,程序就不会局限于单一的工作。
而多线程编程正式为了解决这个问题,如在同一个进程内,比如QQ,我可以一边聊天,一边去下载群里的文件,同时也可以一边上传文件。这就用到了多线程的技术,让程序不局限于单一的工作,利用多余的CPU资源去同时工作,提升用户的体验,这也是图形化系统提升用户体验的最佳实践。
而进程却和线程有所不同,比如我可以一边写博客(浏览器)、一边听歌(网易云)、一边聊天(QQ、微信)。这里用到了多个不同的程序 ,每个程序都互相独立的工作,在没有进程通信时,大多情况下都不会影响对方工作。我们可以打开任务管理器,可以看到操作系统下的大量进程在同时工作,这就是多进程的概念。
-
程序、进程、线程概念
- 程序(program),是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
- 进程(process),是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。
- 线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。 若一个程序可同一时间执行多个线程,就是支持多线程的
-
何时应用多线程?
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 程序需要一些后台运行的程序时。
-
实现方式
一、继承Thread类
步骤:
- 定义子类继承Thread类
- 子类中重写Thread类中的run方法
- 创建Thread子类对象,即创建了线程对象
- 调用线程对象start方法:启动线程,调用run方法
代码:
public class TestThread {
public static void main(String[] args) {
MyThread1 th1 = new MyThread1();
th1.start();
/**
* 主线程
*/
for (int i = 10; i >= 0; i--) {
System.out.println("Main-" + i);
}
}
}
class MyThread1 extends Thread {
public MyThread1() {
}
@Override
public void run() {
for (int i = 10; i >= 0; i--) {
System.out.println("MyThread1-" + i);
}
}
}
二、实现Runnable接口
步骤:
- 定义子类,实现Runnable接口
- 子类中重写Runnable接口中的run方法
- 通过Thread类含参构造器创建线程对象
- 将Runnable接口的子类对象作为实际参数传递给 Thread 类的构造方法中
- 调用Thread类的start方法:开启线程,调用 Runnable子类接口的run方法
代码:
public class TestThread {
public static void main(String[] args) {
MyThread2 th2 = new MyThread2();
Thread thread = new Thread(th2);
thread.start();
/**
* 主线程
*/
for (int i = 10; i >= 0; i--) {
System.out.println("Main-" + i);
}
}
}
class MyThread2 implements Runnable {
public void run() {
for (int i = 10; i >= 0; i--) {
System.out.println("MyThread2-" + i);
}
}
}
实现 Runnable 接口的优点:
- Java 是单继承的,用实现接口的方式可以避免单继承的局限问题
- 只需 new 一个实现 Runnable 接口的实例,保证了可以共享同一份资源
-
线程重要内容
一、常用方法
- void start(); 启动线程,并执行对象的run()方法
- run(); 线程在被调度时执行的操作
- String getName(); 返回线程的名称
- void setName(String name); 设置该线程名称
- static currentThread(); 返回当前线程
修改如上代码:
public class TestThread {
public static void main(String[] args) {
/**
* 继承 Thread 的方式
*/
MyThread1 th1 = new MyThread1();
th1.setName("==th1==");
th1.start();
/**
* 实现 Runnable 接口的方式
*/
MyThread2 th2 = new MyThread2();
Thread thread = new Thread(th2);
thread.setName("==th2==");
thread.start();
/**
* 主线程
*/
for (int i = 10; i >= 0; i--) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
class MyThread1 extends Thread {
public MyThread1() {
}
@Override
public void run() {
for (int i = 10; i >= 0; i--) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
class MyThread2 implements Runnable {
public void run() {
for (int i = 10; i >= 0; i--) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
二、线程调度
Java对线程的调度方法:
- 对于同优先级线程,组成一个队列,以先进先出的方式抢占CPU资源
- 对于高优先级的线程,赋予优先的抢占式资源(但是也不是绝对的能够抢到)
1、设置线程优先级
线程的优先级分为三个等级,分别为 MAX_PRIORITY(10); MIN _PRIORITY (1); NORM_PRIORITY (5);通过:
- getPriority() :返回线程优先值,默认为5
- setPriority(int newPriority) :改变线程的优先级,线程创建时继承父线程的优先级
2、yieid()、join()、sleep()
- yield():线程让步,暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程。若队列中没有同优先级的线程,忽略此方法。
- join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止,低优先级的线程也可以获得执行 。
- sleep(long millis)(毫秒) : 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
- isAlive():判断线程是否还活着
例子:实现有三个人同时去银行取款机取钱,这三个同时操作。设银行共有10000元,第一人取1500,第二人取2000,第三人取3000,结算银行剩余多少钱?
这个问题比较简单,但是存在一个bug,线程抢夺cpu资源的问题。如果第一个人在取的时候,恰巧cpu资源权被第二个人抢了,那就造成问题。
public class TestThread {
public static void main(String[] args) {
Bank bank = new Bank();
bank.getMoney(1500);
Thread person1 = new Thread(bank);
person1.setName("==person1==");
person1.start();
try {
person1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
bank.getMoney(2000);
Thread person2 = new Thread(bank);
person2.setName("==person2==");
person2.start();
try {
person2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
bank.getMoney(3000);
Thread person3 = new Thread(bank);
person3.setName("==person3==");
person3.start();
try {
person3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Bank implements Runnable {
int totalMoney = 10000;
int money;// 要取出的钱
public void getMoney(int money) {
this.money = money;
}
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "取出:" + money);
int remainMoney = totalMoney - money;
totalMoney = remainMoney;
System.out.println("银行剩余:" + remainMoney);
}
}
- 正常执行结果等于3500:
如果注释上面代码中线程的 join() 方法,意味着三个人对cpu的获取权一样大,比如第一个人取到一半,执行权被第二个人给抢了,这就会导致金钱出现异常。
- 注释全部 join() 后执行结果:
三、线程生命周期
四、线程同步(synchronized)
线程同步指的是同一个线程来操作同一份资源,那不同步就有线程安全问题了。在线程并发时,如果不同的多个线程同时操作同一封资源的话,那将会造成数据紊乱。
举个实际中的例子,我在乘坐高铁时想上厕所,这时厕所显示绿色,发现厕所没人用,我就进去了,却不小心门没有关紧。这时又来了一位想上厕所的人,由于门没关好,厕所上面的灯是绿色的,所以这位后面来的人就开门进来了,这就导致厕所紊乱了。
用这个例子反证线程的执行过程,简直一模一样。这个厕所,就如线程处理的同一份资源。多个人就对应多个线程,在同时处理一份资源时,问题就来了。
互斥锁(synchronized),这是一个关键字。作用在同一份资源上时,就是相当于厕所上面的指示器的作用,给这个资源加上一把锁,你其他线程不许进来,等我处理结束后再说。
看一个例子:
一家电影院有三个售票窗口,这部电影共有30个座位(30张票)。如果三个窗口同时卖票,则该如何操作?
public class TestThread {
public static void main(String[] args) {
Cinema cinema = new Cinema();
Thread window1 = new Thread(cinema);
window1.setName("==窗口1==");
window1.start();
Thread window2 = new Thread(cinema);
window2.setName("==窗口2==");
window2.start();
Thread window3 = new Thread(cinema);
window3.setName("==窗口3==");
window3.start();
}
}
class Cinema implements Runnable {
int ticket = 30;
public void run() {
String name = Thread.currentThread().getName();
while (true) {
if (ticket > 0) {
try {
Thread.currentThread().sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "售出" + ticket-- + "号座位");
}
}
}
}
从打印结果可以看出,不仅出现了重复座位,而且还出现了 0 号座位,存在极大的bug。出现bug的原因:多个线程参与同一个数据的操作,如上代码中,多个线程同时卖30张票,却没有给操作同一个资源加锁,就会出现这种bug。
1、同步代码块
修改 run(),添加 synchronized(Object obj) 关键字。这里一般传入 this ,this 即 Cinema 类的对象。
class Cinema implements Runnable {
int ticket = 30;
public void run() {
while (true) {
synchronized (this) {
if (ticket > 0) {
try {
Thread.currentThread().sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售出" + ticket-- + "号座位");
}else {
break;
}
}
}
}
}
2、同步方法
class Cinema implements Runnable {
int ticket = 30;
boolean flag = true;
public void run() {
while (flag) {
sell();
}
}
public synchronized void sell() {
if (ticket > 0) {
try {
Thread.currentThread().sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售出" + ticket-- + "号座位");
flag = true;
} else {
flag = false;
}
}
}
同步代码块、同步方法都可以决解线程安全问题。其实,线程安全的单利模式也是可以的,只要保证操作资源的线程同一时间内是唯一的就可以了。
结果正常:
注意:线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,此时并不会释放锁。
五、死锁
说到调用 sleep() 方法不会释放锁,那么如果多个线程同时操作对方的资源,谁都不愿意释放的话,那程序就会停止,就会造成死锁的情况了。死锁就是不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
举个实际例子:有一天,老王和老张在工地里吃饭,恰巧剩下最后一双筷子。老王拿了一根,老张拿了一根(就当这俩有毛病吧)。于是老张在等待老王放弃筷子,那么老王也想让老张先放弃。这时,他们俩就抢了起来啊,然后咔嚓一声,其中一根断了(意味着程序bug),那么这俩货就这样僵持住了,谁也吃不了。
死锁代码:
public class TestDeadlock implements Runnable {
Zhang zhang = new Zhang();
Wang wang = new Wang();
public void init() {
zhang.waitting(wang);
}
public static void main(String[] args) {
System.out.println("老张、老王各有一根筷子");
TestDeadlock dl = new TestDeadlock();
new Thread(dl).start();
dl.init();
}
@Override
public void run() {
wang.waitting(zhang);
}
}
class Zhang {
public synchronized void waitting(Wang wang) {
try {
Thread.currentThread().sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
wang.eat();
}
public synchronized void eat() {
System.out.println("老张想吃饭,等待老王给筷子");
}
}
class Wang {
public synchronized void waitting(Zhang zhang) {
zhang.eat();
}
public synchronized void eat() {
System.out.println("老王想吃饭,等待老张给筷子");
}
}
死锁的原因就是不同的线程分别占了对方也需要的资源,这时谁也不肯退让,导致程序停止。我们不可能去专门编写死锁,但出现死锁时就要我们去解救。解决方法:专门的算法、原则。或者尽量减少同步资源的定义。
六、线程通信
线程的通信,通过wait() 与 notify() 和 notifyAll()三个方法实现。所谓通信,就是某一个线程被wait()之后,其他线程通过notify()和notifyAll()将其唤醒。wait()不同于sleep(),这一点很重要。sleep()方法可以通过自定义的一段时间后自动唤醒,而wait()只能被notify的时候才可以苏醒,否则线程将进入停滞状态。
- wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,当前线程排队等候再次对资源的访问
- notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
- notifyAll ():唤醒正在排队等待资源的所有线程结束等待
例子:打印整数,实现单双号交替
public class TestThread3 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread th1 = new Thread(myThread);
th1.setName("==单数==");
th1.start();
Thread th2 = new Thread(myThread);
th2.setName("==双数==");
th2.start();
}
}
class MyThread implements Runnable {
int count = 21;
public void run() {
while (true) {
synchronized (this) {
notify();
if (count > 0) {
System.out.println(Thread.currentThread().getName() + count--);
} else {
break;
}
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
到此为止,线程的几个内容已经基本讲完了。掌握这些线程的知识点,可以开发出更加高效的软件,多线程编程也是能写出更高效软件的一种手段。