多线程(一)
一、进程和线程
1.区别和联系
区别:
进程是一个正在进行中的程序。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或一个控制单元。进程是系统进行资源分配和调度的一个独立单位。每一个应用程序启动的时候,都会被分配一定的内存空间。进程是用于标识这片内存空间,用于封装内存空间里的控制单元。
线程是进程中一个独立的控制单元,控制着进程的执行。它是CPU分配和调度其资源的基本单位。线程没有办法脱离进程独立运行,必须包含在进程中运行。多线程之间共享进程拥有的资源。
联系:
进程和线程都是系统创建的。一个进程中至少有一个线程。同一个进程中的多个线程可以并发执行,提高程序运行效率。
示例代码:
class ThreadTest {
public static void main(String[] args) {
for(int x = 0; x < 4000; x ++) {
System.out.println("Hello World...");
}
}
}
用dos命令行编译上述代码时,会启动javac
进程;运行上述代码时,会启动java
进程,该进程中至少有一个线程在负责java程序的执行,而且这个线程运行的代码存在于main
方法中,该线程称为主线程。
小扩展
jvm启动的时候,不止一个线程,还有负责垃圾回收机制的进程。
二、多线程
1.多线程存在的意义
多线程的出现能让程序产生同时运行的效果。
2.自定义线程
Java提供了对线程这类事物的描述,封装为Thread
类。创建线程有两种方式:
1)继承Thread
类,复写run()
方法,调用线程的start()
方法
示例代码:
//继承线程类
class TestThread extends Thread {
//复写run()方法
public void run() {
System.out.println("TestThread running...");
}
}
class Test {
public static void main(String[] args) {
//创建线程对象
TestThread myThread = new TestThread();
//线程开始执行
myThread.start();
}
}
start()
方法启动线程,调用run()
方法;修改代码,分析打印结果。
示例代码:
class TestThread extends Thread {
public void run() {
for(int x = 0; x < 60; x++) {
System.out.println("...TestThread running" + x);
}
}
}
class Test {
public static void main(String[] args) {
TestThread myThread = new TestThread();
myThread.start();
for(int x = 0; x < 60; x++) {
System.out.println("MainThread running..." + x);
}
}
}
程序运行部分结果:
MainThread running...0
...TestThread running0
...TestThread running1
MainThread running...1
MainThread running...2
MainThread running...3
运行结果每次都不同,因为多个线程都在获取CPU的执行权(资源);CPU执行哪个线程,就运行哪个线程里的代码;某一时刻,只有一个线程在运行(多核除外);CPU在做着快速切换,达到了看上去程序是同时运行的效果。这是多线程的随机性。如图:
程序开始运行,主线程启动,随后创建好的myThread
线程启动,负责执行run()
方法中的代码;主线程负责执行main()
方法中的代码;CPU分配资源,交替执行两个线程,所以有如上运行结果。
小扩展1
复写run()方法的原因
Thread类用于描述线程,该类定义了一个功能用于存储线程要运行的代码,这个功能就是run()方法。小扩展2
直接调用子类中的run()方法,程序的运行结果
示例代码:
class TestThread extends Thread {
public void run() {
for(int x = 0; x < 60; x++) {
System.out.println("...TestThread running" + x);
}
}
}
class Test {
public static void main(String[] args) {
TestThread myThread = new TestThread();
//直接调用run()方法,而非start()方法
myThread.run();
for(int x = 0; x < 60; x++) {
System.out.println("MainThread running..." + x);
}
}
}
程序运行结果为:
...TestThread running0
...TestThread running1
...TestThread running2
...TestThread running3
...TestThread running4
...TestThread running5
.
.
.
MainThread running...0
MainThread running...1
MainThread running...2
MainThread running...3
MainThread running...4
MainThread running...5
.
.
.
如果直接调用run()
方法,该程序并没有启动新的线程,而整个程序是由主线程完成执行的。myThread
线程虽然被创建,但是始终没有启动。如图:
主线程执行到run()
方法,就将run()
方法中的代码先执行完,再回到main
方法中,执行main
方法中的代码;主线程独享CPU资源;直接调用run()
方法的效果,相当于对象调用方法,没有多线程执行的特点。
小练习1
创建两个线程,和主线程交替运行。
示例代码:
package com.heisejiuhuche;
class TestThread extends Thread {
private String name;
TestThread(String name) {
this.name = name;
}
//复写run()方法
public void run() {
for(int x = 0; x < 20; x++) {
System.out.println(name + " TestThread running" + x);
}
}
}
class Test {
public static void main(String[] args) {
//创建两个线程对象
TestThread tt1 = new TestThread("***");
TestThread tt2 = new TestThread("---");
//启动tt1和tt2线程
tt1.start();
tt2.start();
for(int x = 0; x < 20; x++) {
System.out.println("MainThread running..." + x);
}
}
}
程序部分运行结果为:
*** TestThread running4
--- TestThread running10
MainThread running...5
--- TestThread running11
*** TestThread running5
--- TestThread running12
MainThread running...6
--- TestThread running13
小练习2
用Thread类的getName()方法,打印线程名称
示例代码:
package com.heisejiuhuche;
class TestThread extends Thread {
private String name;
TestThread(String name) {
this.name = name;
}
//复写run()方法
public void run() {
for(int x = 0; x < 20; x++) {
//用Thread类的getName()方法,打印线程名称
System.out.println(this.getName() + " TestThread running" + x);
}
}
}
class Test {
public static void main(String[] args) {
//创建线程对象
TestThread tt1 = new TestThread();
//启动tt1线程
tt1.start();
for(int x = 0; x < 20; x++) {
System.out.println("MainThread running..." + x);
}
}
}
程序部分运行结果为:
Thread-0 TestThread running9
MainThread running...2
Thread-0 TestThread running10
MainThread running...3
Thread-0 TestThread running11
Thread-0
即为tt1
线程名称。每个线程有自己默认的名称,即Thread-[编号]
。标准通用的获取当前线程名称的方法是使用Thread
类的currentThread()
方法:
Thread.currentThread().getName();
currentThread()
方法为静态方法,获取当前线程对象,返回Thread
类型。如果要自定义名称,使用Thread
类的setName()
方法或使用线程构造方法即可。
2)实现Runnable接口
用多线程实现一个简单的多窗口卖票程序。
示例代码:
package com.heisejiuhuche;
public class TicketBooth {
public static void main(String[] args) {
//创建四个线程对象,并启动
TicketThread tt1 = new TicketThread();
TicketThread tt2 = new TicketThread();
TicketThread tt3 = new TicketThread();
TicketThread tt4 = new TicketThread();
tt1.start();
tt2.start();
tt3.start();
tt4.start();
}
}
class TicketThread extends Thread {
//四个窗口共享100张票
private static int ticket = 100;
public void run() {
//票大于0的时候,四个窗口同时卖票
while(true) {
if(ticket > 0) {
System.out.println(Thread.currentThread().getName()
+ "Selling ticket: " + ticket--);
}
}
}
}
程序部分运行结果为:
Thread-2Selling ticket: 2
Thread-0Selling ticket: 3
Thread-3Selling ticket: 4
Thread-1Selling ticket: 1
上述程序声明了静态成员,由于静态成员声明周期过长,一般不建议使用静态属性。如果没有静态,则每个线程对象里有100
张票,总共将卖出400
张。要解决这个问题,就要使用线程的第二种创建方法:实现Runnable
接口。
实现Runnable接口的步骤:
1> 声明一个类,实现Runnable接口
示例代码:
class Ticket implements Runnable {
}2> 复写Runnable接口中的run()方法,将线程要运行的代码存放在run()方法中
示例代码:
class Ticket implements Runnable {
public void run() {}}
3> 创建Thread线程对象,并将Runnable接口的子类对象作为实际参数传给Thread类的构造方法
示例代码:
Ticket ticket = new Ticket();
Thread thread = new Thread(ticket);4> 调用线程对象的start()方法启动线程,并调用Runnable接口子类中的run()方法
示例代码:
thread.start();
上述步骤中,第3
步是重点。因为通过Thread thread = new Thread();
创建的thread
对象,运行的是Thread
类中的run()
方法,而这个run()
方法里没有任何代码可以运行;需要运行的代码,写在了Runnable
接口子类复写的run()
方法中;那么,在创建线程对象时,要明确该线程启动之后要运行的代码,就应该在创建线程时,把拥有可运行代码的run()
方法所属的对象(Runnable子类
),传给线程对象。这是Thread
类的构造方法之一,接收一个Runnable类型的对象作为参数:Thread thread = new Thread(ticket);``ticket
是Runnable
接口的子类对象。这样,thread
线程对象启动之后,就有了可以执行的代码,该代码在ticket
复写父类的run()
方法中。
完整代码:
package com.heisejiuhuche;
public class TicketBooth {
public static void main(String[] args) {
//创建TicketThread对象
TicketThread tt = new TicketThread();
//将tt传给Thread类,创建线程并启动
new Thread(tt).start();
new Thread(tt).start();
new Thread(tt).start();
new Thread(tt).start();
}
}
class TicketThread implements Runnable {
//多线程共享100张票
private static int ticket = 100;
public void run() {
//票大于0的时候,四个窗口同时卖票
while(true) {
if(ticket > 0) {
System.out.println(Thread.currentThread().getName()
+ "Selling ticket: " + ticket--);
}
}
}
}
程序运行部分结果:
Thread-3Selling ticket: 2
Thread-1Selling ticket: 3
Thread-0Selling ticket: 4
Thread-2Selling ticket: 1
无需加静态,四个线程共享100
张票的数据。
3)两种创建线程方式的区别
1> 实现Runnable接口
实现的方式避免了单继承的局限性,同时让资源独立共享(实现的方式中,100
张票是被四个线程共享)。以学生,工人和人的关系为例,学生继承了人,现在学生类当中有一部分代码,需要多线程的支持。但是Java只支持单继承,学生已经无法再继承Thread
类来获取多线程功能。那么这时,Runnable
接口就可以让学生在单继承模式下,同时获得多线程的功能。学生只需实现Runnable
接口即可。开发中,建议使用实现方式创建多线程。
2> 继承Thread
类,线程代码存放于Thread
子类的run()
方法中;实现Runnable
接口,线程代码存放于Runnable
接口子类的run()
方法中
3.线程运行状态
线程有4个一般状态和1个临时状态:
1)被创建的状态;
2)运行状态;线程被创建之后,通过调用start()方法启动,进入运行状态;
3)冻结状态;冻结状态可以理解为无执行资格的状态;调用了sleep(),wait()方法的线程,会进入冻结状态;冻结状态的线程接收到notify(),即脱离冻结状态,此时如果获得执行权限,则转入运行状态;如果没有获得执行权限,则转入临时阻塞状态;
4)消亡状态;运行中的线程调用stop()方法,或run()方法结束,线程转入消亡状态;
5)临时阻塞状态;转入运行状态的线程,如果丧失CPU执行权限,则转入阻塞状态;重新获得CPU执行权限,则返回运行状态
4.多线程安全问题
1)多线程安全问题模拟
示例代码:
package com.heisejiuhuche;
public class TicketBooth {
public static void main(String[] args) {
TicketThread tt = new TicketThread();
new Thread(tt).start();
new Thread(tt).start();
new Thread(tt).start();
new Thread(tt).start();
}
}
class TicketThread implements Runnable {
private static int ticket = 100;
public void run() {
while(true) {
if(ticket > 0) {
try {
//让当前线程睡眠10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "Selling ticket: " + ticket--);
}
}
}
}
程序运行部分结果:
Thread-2Selling ticket: 2
Thread-0Selling ticket: 1
Thread-1Selling ticket: 0
Thread-3Selling ticket: -2
Thread-2Selling ticket: -1
上述代码打印出了0
,-1
,-2
号错票,多线程的运行出现了安全问题。加上Thread.sleep(10);
这行代码,为了放大效果。安全隐患产生的原因如下,当执行:
if(ticket > 0) {
System.out.println(Thread.currentThread().getName() + "Selling ticket: " + ticket--);
}
这块代码的时候,假设此时ticket = 1
;0
号线程判断ticket > 0
成立,但是还没等执行打印语句,CPU切换到了1
号线程,1
线程判断ticket > 0
成立,准备执行打印语句的时候,CPU又切换到了2
号线程;2
号线程判断ticket > 0
成立,打印:
Thread-2Selling ticket: 1
此时ticket = 0;
如果这时CPU执行0
号线程,则打印:
Thread-0Selling ticket: 0
此时ticket = -1
;CPU再执行1
号线程,打印:
Thread-1Selling ticket: -1
以此类推,线程越多,票越多,这样的代码产生的错票越多,安全问题越严重。当多个线程在操作同一共享数据时,一个线程还没有执行完,另一个线程参与执行,导致共享数据错误。解决此类问题,只需在某一时间段,仅让一个线程操作共享数据,这个过程中,其他线程不能参与执行。Java对于对线程的安全问题,提供了专业解决方式。
2)同步代码块(synchronized关键字)
格式:
synchronized(对象) {
需要被同步的代码;
}
同步代码块示例代码:
package com.heisejiuhuche;
public class TicketBooth {
public static void main(String[] args) {
TicketThread tt = new TicketThread();
new Thread(tt).start();
new Thread(tt).start();
new Thread(tt).start();
new Thread(tt).start();
}
}
class TicketThread implements Runnable {
private static int ticket = 100;
Object obj = new Object();
public void run() {
while(true) {
//同步代码块
synchronized(obj) {
if(ticket > 0) {
System.out.println(Thread.currentThread().getName()
+ "Selling ticket: " + ticket--);
}
}
}
}
}
程序运行部分结果:
Thread-2Selling ticket: 5
Thread-2Selling ticket: 4
Thread-2Selling ticket: 3
Thread-2Selling ticket: 2
Thread-2Selling ticket: 1
由于票的数量比较少,可能出现有线程无法抢到CPU执行权的情况。同步代码块的对象参数,相当于一把锁,持有锁的线程,可以执行同步代码块中的代码;没有锁的线程,即使获取CPU的执行权,也不能进入同步代码块进行操作。
3)同步的前提
1> 必须有两个或两个以上线程;
2> 必须是多线程使用同一个锁;
4)同步的利弊
1> 利:
解决了多线程的安全问题
2> 弊:
每次都要判断锁的状态,较消耗系统资源
5)同步方法
同步方法是同步的另一种表现形式。同步方法只需要在函数声明时加上synchronized
关键字即可。
格式如下:
public synchronized void method() {}
小练习
银行有一个金库,有两个储户分别存300,每次存100,分存3次,用多线程实现。
示例代码:
package com.heisejiuhuche;
public class BankTest {
public static void main(String[] args) {
Customer c = new Customer();
//启动线程
new Thread(c).start();
new Thread(c).start();
}
}
//Customer类实现Runnable接口
class Customer implements Runnable {
private Bank b = new Bank();
private final int NUM = 100;
//复写run()方法,run()方法调用Bank类的add()方法
public void run() {
for(int x = 0; x < 3; x++) {
b.add(NUM);
}
}
}
class Bank {
private int sum = 0;
//add()方法每次让sum加100
void add(int num) {
sum += num;
System.out.println("sum = " + sum);
}
}
程序运行结果:
sum = 200
sum = 200
sum = 400
sum = 300
sum = 500
sum = 600
没有输出100
的原因就在于,这段代码:
sum += num;
System.out.println("sum = " + sum);
出现了多线程安全问题。假设sum = 0;
当前一个线程执行了sum += num;
之后丧失了执行权;此时sum = 100;
下一个线程抢到了执行权,执行了这两句代码,sum = 200;
那么上一个线程重新获得执行权输出的时候,就不会输出sum = 100;
而又输出sum = 200;
在写多线程程序时,要注意:
1> 明确哪些代码是多线程要运行的代码;(两个run()方法)
2> 明确共享数据;(Bank的b对象和sum变量)
3> 明确多线程运行代码中哪些语句是操作共享数据的(add()方法中的代码)
因此,应该修改Bank类中的add()方法中的代码为同步代码块。
示例代码:
class Bank {
private int sum = 0;
Object obj = new Object();
//add()方法每次让sum加100
void add(int num) {
//声明同步代码块
synchronized(obj) {
sum += num;
System.out.println("sum = " + sum);
}
}
}
做出修改之后的程序运行结果:
sum = 100
sum = 200
sum = 300
sum = 400
sum = 500
sum = 600
也可以使用同步方法:
class Bank {
private int sum = 0;
Object obj = new Object();
//add()方法每次让sum加100
public synchronized void add(int num) { //同步方法
sum += num;
System.out.println("sum = " + sum);
}
}
6)同步方法——this锁
同步方法中,没有obj
对象,但是同步方法也有锁的特性。由于方法要被对象调用,那么每个方法都有一个所属对象的引用this
,所以同步方法使用的锁就是this
。
验证同步方法使用的是this锁:
下面的代码设置了一个flag
,让t1
和t2
线程在同步代码块和同步方法之间交替执行:
package com.heisejiuhuche;
public class TicketBooth {
public static void main(String[] args) {
TicketThread tt = new TicketThread();
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
// 启动t1线程,t1线程在同步代码块里执行
t1.start();
try {
// 启动t1线程后,让主线程休眠10毫秒,为了确保t1和t2能交替执行
Thread.sleep(10);
} catch (Exception e) {
}
// 主线程让t1线程启动后,让flag为false
tt.flag = false;
// 启动t2线程,t2线程在同步方法里执行
t2.start();
}
}
class TicketThread implements Runnable {
private static int ticket = 100;
Object obj = new Object();
boolean flag = true;
public void run() {
if (flag) {
while (true) {
// 同步代码块
synchronized (obj) {
if (ticket > 0) {
try {Thread.sleep(10);} catch(Exception e) {}
System.out.println(Thread.currentThread().getName()
+ "Run Selling ticket: " + ticket--);
}
}
}
} else {
while (true) {
// 同步方法
show();
}
}
}
private synchronized void show() {
if (ticket > 0) {
try {Thread.sleep(10);} catch(Exception e) {}
System.out.println(Thread.currentThread().getName()
+ "Show Selling ticket: " + ticket--);
}
}
}
程序运行部分结果:
Thread-0Run Selling ticket: 4
Thread-1Show Selling ticket: 3
Thread-0Run Selling ticket: 2
Thread-1Show Selling ticket: 1
Thread-0Run Selling ticket: 0
虽然同步,但是还是出现了0
号票的错误;同步之后还出现多线程安全问题,一定是同步的前提没有满足。两个或两个以上的前提满足了,但是用一个锁的前提没有满足。同步代码块用的是obj
锁,同步方法用的,应该是this
锁(假设阶段)。那么,现在让同步代码块也使用this
锁,如果程序运行没有问题,就验证了同步方法使用的是this
锁。
修改代码如下:
if (flag) {
while (true) {
synchronized (this) {
if (ticket > 0) {
try {Thread.sleep(10);} catch(Exception e) {}
System.out.println(Thread.currentThread().getName()
+ "Run Selling ticket: " + ticket--);
}
}
}
}
将this
代替obj
传给同步代码块,程序运行结果如下:
Thread-0Run Selling ticket: 7
Thread-0Run Selling ticket: 6
Thread-1Show Selling ticket: 5
Thread-1Show Selling ticket: 4
Thread-1Show Selling ticket: 3
Thread-1Show Selling ticket: 2
Thread-1Show Selling ticket: 1
程序运行没有问题,验证了同步方法使用的是this
锁。
7)静态同步方法——Class对象锁
将show()方法声明为静态,分析程序运行结果:
private static synchronized void show() {
if (ticket > 0) {
try {Thread.sleep(10);} catch(Exception e) {}
System.out.println(Thread.currentThread().getName()
+ "Show Selling ticket: " + ticket--);
}
}
此时,同步代码块中,仍旧使用this锁:
synchronized (this) {
if (ticket > 0) {
try {Thread.sleep(10);} catch(Exception e) {}
System.out.println(Thread.currentThread().getName()
+ "Run Selling ticket: " + ticket--);
}
}
程序运行结果为:
Thread-0Run Selling ticket: 2
Thread-1Show Selling ticket: 1
Thread-0Run Selling ticket: 0
程序出现多线程安全问题。如果同步方法被静态修饰,使用的锁不是this
,因为静态方法中没有this
对象。类加载进内存的时候,先被编译成字节码文件对象,然后静态成员加载进内存。此时,内存中没有本类对象,只有一个已被编译的类的字节码文件对象(类名.class
)。由此推测,静态同步方法使用的是类名.class
锁。
修改程序,将
类名.class
代替this
传给同步代码块,如果程序运行正常,证明推论正确:
synchronized (TicketThread.class) { //同步代码块所在类为 TicketThread类
if (ticket > 0) {
try {Thread.sleep(10);} catch(Exception e) {}
System.out.println(Thread.currentThread().getName()
+ "Run Selling ticket: " + ticket--);
}
}
程序运行结果为:
Thread-1Show Selling ticket: 7
Thread-1Show Selling ticket: 6
Thread-1Show Selling ticket: 5
Thread-0Run Selling ticket: 4
Thread-0Run Selling ticket: 3
Thread-0Run Selling ticket: 2
Thread-0Run Selling ticket: 1
多次测试,程序运行正常;静态同步方法使用的锁是该方法所在类的字节码文件对象,也就是类名.class
。
三、线程死锁
1.定义
两个线程分别持有各自的锁,一个线程想执行另一个线程的代码块,需要取得另一个线程的锁;而另一个线程想做同样的事情;两个线程互相不放弃自己的锁,又同时需要对方的锁的现象,就是线程死锁。线程死锁会导致程序无响应。
2.线程死锁的原因
线程死锁通常出现于同步中嵌套同步,而使用的锁又不同的情况。
示例代码:
package com.heisejiuhuche;
public class DeadLockTest {
public static void main(String[] args) {
Thread t1 = new Thread(new DeadLock(true));
Thread t2 = new Thread(new DeadLock(false));
t1.start();
t2.start();
}
}
class DeadLock implements Runnable {
private boolean flag;
DeadLock(boolean flag) {
this.flag = flag;
}
public void run() {
//判断语句让一个线程在if里运行,一个在else里运行
if(flag) {
//同步代码块的嵌套使用不同的锁,产生死锁现象
synchronized(Lock.lockA) {
System.out.println("If LockA---------------");
synchronized(Lock.lockB) {
System.out.println("If LockB**************");
}
}
} else {
synchronized(Lock.lockB) {
System.out.println("IF LockB*************");
synchronized(Lock.lockA) {
System.out.println("If LockA---------------");
}
}
}
}
}
//Lock类,负责产生两个不同的锁
class Lock {
static Object lockA = new Object();
static Object lockB = new Object();
}
程序运行结果:
IF LockB*************
If LockA---------------
打印两行之后,程序无响应,线程死锁现象产生。开发中要避免线程死锁的情况。
四、单例设计模式
前方高能(面试用):
: 懒汉式是实例的延时加载;
懒汉式延时加载在多线程访问时会出现安全问题,可以用双重判断加同步的方式,较高效解决安全问题;
懒汉式加同步时,使用的锁是该类的字节码文件对象,即类名.class
示例代码:
class Single {
private static Single s = null; //延时加载
private Single() {}
public static Single getInstance() {
//双重判断,提高效率
if(s == null) {
//同步代码块,解决多线程安全问题
synchronized(Single.class) {
if(s == null) {
s = new Single();
}
}
}
return s;
}
}