一、本章要点
1. 线程的创建
2. 线程与进程的区别
3. 线程的生命周期
4. 线程控制
5. 线程安全问题
6. 线程间通信
7. ThreadLocal
1.线程的创建
1. 扩展java.lang.Thread类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class MyThread extends Thread {
private String threadName;
public MyThread(String _threadName){
this.threadName = _threadName;
}
public void run() {
for(int i = 0;i<100;i++){
System.out.print(threadName + "i = " + i);
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
MyThread t1 = new MyThread("thread-A");
MyThread t2 = new MyThread("thread-B");
t1.start();
t2.start();
}
}
输出:
thread-Ai = 0
thread-Bi = 0
thread-Ai = 1
thread-Bi = 1
thread-Bi = 2
thread-Ai = 2
thread-Bi = 3
thread-Ai = 3
thread-Bi = 4
thread-Ai = 4
- start()方法被调用后并不是立即执行多线程代码,而是将该线程变为可运行状态(Runnable),什么时候运行业务代码是由操作系统决定的。
- 从程序的运行结果看,多线程程序是乱序执行,哪个线程获取CPU执行权限,哪个线程就执行。
- sleep()是静态方法。
- start()方法重复调用,会出现java.lang.IllegalThreadStateException异常。
2. 实现java.lang.Runnable接口1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public class MyRunnable implements Runnable {
private String threadName;
public MyRunnable(String _threadName){
this.threadName = _threadName;
}
public void run() {
for(int i = 0;i<5;i++){
System.out.println(threadName + "i = " + i);
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
new Thread(new MyRunnable("thread-C")).start();
new Thread(new MyRunnable("thread-D")).start();
}
}
输出:
thread-Di = 0
thread-Ci = 0
thread-Di = 1
thread-Ci = 1
thread-Ci = 2
thread-Di = 2
thread-Ci = 3
thread-Di = 3
thread-Di = 4
thread-Ci = 4
- 启动多线程时,需要通过Thread类的构造函数Thread(Runnable target)构造出对象,然后调用start()方法将线程变为可运行状态。
2. 线程与进程的区别
进程和线程的主要差别在于它们是操作系统进行资源管理的不同方式。
- 一个程序至少有一个进程,一个进程至少有一个线程。
- 线程的划分尺度小于进程,使得多线程程序的并发性高。
- 进程在执行过程中拥有独立的内存单元,而多个线程共享进程的资源,从而极大地提高了程序的运行效率。
- 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。
3. 线程的生命周期
当一个线程被创建并启动后,它主要会经过新建(new)、就绪(runnable)、运行(running)、阻塞(blocked)和死亡(dead)五种状态。
- 使用new关键字创建一个线程之后,该线程就处于新建状态。仅由Java虚拟机分配内存,并初始化其成员变量的值,没有任何线程的动态特征。
- 调用start()方法后,线程处于就绪状态。此时的线程并没有开始运行,只是表示线程可以运行了。具体何时开始运行取决于虚拟机的调度。
- 处于就绪状态的线程获取了处理器资源,就会开始执行run()方法中的代码块。在任何时刻只有一个线程处于运行状态(单处理器)。
- 当线程失去处理器资源或者调用yield()方法,线程会回到就绪状态。
- 运行—>阻塞
①、调用sleep()方法主动放弃处理器资源
②、线程调用阻塞式IO方法,在该方法返回前,该线程被阻塞
③、线程在等待某个通知notify
④、调用了suspend()方法将该线程挂起,易死锁避免使用 - 阻塞—>就绪
①、sleep()方法到了指定的时间
②、阻塞式IO方法已经返回
③、线程正在等待通知,正好其他线程发出一个通知(不一定)
④、处于阻塞状态的线程被调用resume()方法 - 死亡
①、run()方法执行完毕,线程正常结束
②、线程抛出一个未捕获的异常
③、调用stop()方法结束线程,易死锁不推荐使用
④、可以使用isAlive()方法判断,当线程处于就绪、运行、阻塞时返回true;处于新建、死亡状态时返回false
4. 线程控制
Java提供了便捷的api来控制线程的执行。
- join线程:让一个线程等待另一个线程结束
在某个线程中调用其它线程的join()方法时,调用线程会被阻塞,直到被join()方法加入的线程执行完毕为止。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public class JoinTest extends Thread {
public void run() {
for(int i =0;i<5;i++){
System.out.println("join-thread: "+i);
}
}
public static void main(String[] args){
for(int j = 0;j<6;j++){
if(j == 2){
JoinTest joinTest = new JoinTest();
joinTest.start();
try {
joinTest.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main-thread: "+j);
}
}
}
输出:
main-thread: 0
main-thread: 1
join-thread: 0
join-thread: 1
join-thread: 2
join-thread: 3
join-thread: 4
main-thread: 2
main-thread: 3
main-thread: 4
main-thread: 5
join()方法有以下三种重载:
①、join()等待被加入的线程执行完毕。
②、join(long millis)等待被加入的线程millis毫秒。如果在时间之内仍然没有执行完毕,则不再等待。
③、join(long millis,int nanos)等待被加入的线程millis毫秒+nanos毫微秒。
- 守护线程(Daemon Thread)
守护线程只在后台运行,它的任务是为其他线程提供服务。特征:当所有的前台线程都死亡,守护线程也会死亡。调用线程的setDaemon(true)方法可以将线程设置为守护线程。 - 线程睡眠(sleep)
如果想让当前正在执行的线程暂停一段时间,并进入阻塞状态。则可以调用Thread类的静态方法sleep()。在睡眠的时间段内,线程始终不会获得执行机会,即使系统中没有其他线程在执行。 - 线程让步(yield)
yield与sleep同样都是Thread类的静态方法,都可以让线程暂停,但是yield不会让线程进入阻塞状态,只是将线程置于就绪状态。如果线程得到执行资源那么它会继续执行。
sleep | yield |
---|---|
将线程置于阻塞状态直到睡眠结束 | 将线程置于就绪状态 |
暂停当前线程后,会给其他线程执行机会(不考虑线程优先级) | 只会给线程优先级相同或者优先级更高的线程执行机会 |
抛出InterruptedException异常,需要处理 | 没有声明任何异常 |
可移植性好,不推荐使用yield来控制并发线程的执行 |
- 线程优先级
Thread类提供方法setPriority(int level) (1≤level≤10)方法来设置线程的优先级。Thread.MAX_PRIORITY、Thread.NORM_PRIORITY、Thread.MIN_PRIORITY。
5. 线程安全问题
1.线程安全问题:
类似以下的取款操作,判断余额是否充足/取款/修改账户余额一系列操作在单线程操作时不能会出现问题,但是多线程情况下,由于不是原子操作,所以会出现把账户余额更新成负数的情况。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38public class Account {
private String acctNo; //账号
private double balance; //余额
public Account(String acctNo, double balance) {
this.acctNo = acctNo;
this.balance = balance;
}
public String getAcctNo() {
return acctNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public int hashCode() {
return acctNo.hashCode();
}
public boolean equals(Object obj) {
if(this == obj)
return true;
if(obj != null &&obj.getClass() == Account.class){
Account target = (Account) obj;
return target.getAcctNo().equals(acctNo);
}
return false;
}
}
1 | public class DrawThread extends Thread { |
1 | public class DrawTest { |
取款金额:60.0
取款金额:60.0
余额为:-20.0
余额为:40.0
这类问题的原因大多数是因为单个操作的颗粒度较小,例如取款中:①、获取账户余额;②、判断余额是否充足;③、更新余额。这明显是三个独立的操作。可以使用同步机制将颗粒度较小的原子操作包裹成颗粒度较大的操作,完成同步。
- 发生线程安全问题的条件:
- 存在多线程
- 存在共享资源(上例中的账户)
- 存在多个任务来操作共享资源,当前任务(多个串行原子操作)进行到一半时,线程切换
2.同步代码块1
2
3synchronized("锁对象"){
//多个串行的原子操作
}
java程序允许使用任何对象作为同步监视器(锁对象),但通常使用被并发线程访问的共享资源作为锁对象,将上述run方法改造成以下代码就能避免线程安全问题:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void run() {
synchronized (account) {
if (account.getBalance() >= drawAmount) {
//满足取款条件
System.out.println("取款金额:" + drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为:" + account.getBalance());
} else {
System.out.println(getName() + "余额不足,取款失败!");
}
}
}
3.同步方法
同步方法就是使用关键字synchronized修饰的方法。与同步代码块相比,它无需显式地声明锁对象,它的锁对象是方法本身。
使用同步方法可以实现线程安全的类,它们具有以下特征:
- 该类的任何对象都可以被多线程安全的访问
- 每个线程调用该对象的任意方法都能得到正确的结果
- 每个线程调用该对象的任意方法后,该对象的状态依然保持合理
1
2
3
4//class Account
public void synchronized draw(){
//取款逻辑
}
通过同步方法使类Account变成线程安全的类,而不是在run方法中实现取钱的逻辑,这样更加符合面向对象的设计。现实中Account类还应该提供转账、存入等方法来支持账户完备的功能,而不是直接将setBalance()方法暴露出来任人操作。这样可以更好的保证Account对象的完整性和一致性。
可变类的线程安全是以降低效率为代价的,所以同步时应注意:
- 不要对所有的代码进行同步,只对那些会改变竞争资源的方法进行同步。
- 如果该类可能在单线程和多线程两种环境运行,则应该提供该类的两个版本。例如StringBuilder[单线程使用保证性能],StringBuffer[保证多线程下安全]
4.锁对象的释放
任何线程调用同步方法或进入同步代码块时都会获取锁对象,那么锁对象的释放条件如下
- 同步方法、同步代码块执行结束
- 同步方法、同步代码块中遇到break/return后跳出
- 遇见Error/Exception,方法异常结束
- 程序执行了同步监视器的wait()方法,当前线程进入阻塞状态并释放锁对象
- 执行同步方法、同步代码块是线程调用Thread.sleep()[阻塞]/Thread.yield()[就绪]方法将线程暂停时,该点不会释放锁对象
5.死锁
当两个线程相互等待对方释放锁对象就会出现死锁。一旦出现死锁程序不会报错只是所有线程处于阻塞状态,无法继续。
6. 线程间通信
- wait():导致当前线程等待(进入阻塞状态),直到其他线程调用同步监视器的notify()或者notifyAll()方法来唤醒该线程。
- notify():唤醒在此同步监视器上等待的某个线程,被唤醒的线程是随机选择的。
- notifyAll():唤醒在此同步监视器上等待的所有线程。
- wait()/notify()/notifyAll()方法必须写在同步方法或同步代码块中并由锁对象调用。
账户操作存入支取类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52public class AccountNew {
private String accountNo; //账号
private double balance; //余额
private boolean flag = false;
public AccountNew(){}
public AccountNew(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public double getBalance(){
return this.balance;
}
//支取
public synchronized void draw(double drawAmount){
try{
if(!flag){
wait();
}else{
System.out.println(Thread.currentThread().getName()+" 执行支取操作,支取金额:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
flag = false;
notifyAll();
}
}catch(Exception e){
e.printStackTrace();
}
}
//存入
public synchronized void deposit(double depositAmount){
try{
if(flag){
wait(); //阻塞,等待被唤醒
}else{
System.out.println(Thread.currentThread().getName()+" 执行存入操作,存入金额:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
flag = true;
notifyAll();
}
}catch(Exception e){
e.printStackTrace();
}
}
}
支取线程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class DrawThread extends Thread {
private AccountNew accountNew;
private double drawAmount;
public DrawThread(String name, AccountNew accountNew, double drawAmount) {
super(name);
this.accountNew = accountNew;
this.drawAmount = drawAmount;
}
public void run() {
for(int i = 0;i<100;i++){
accountNew.draw(drawAmount);
}
}
}
存入线程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class DepositThread extends Thread {
private AccountNew accountNew;
private double depositAmount;
public DepositThread(String name,AccountNew accountNew, double depositAmount){
super(name);
this.accountNew = accountNew;
this.depositAmount = depositAmount;
}
public void run() {
for(int i = 0;i<100;i++){
accountNew.deposit(depositAmount);
}
}
}
客户端:1
2
3
4
5
6
7
8
9public class App {
public static void main(String[] args){
AccountNew acct = new AccountNew("3303210020198678976",0);
new DrawThread("我",acct,800).start();
new DepositThread("存钱者A",acct,800).start();
new DepositThread("存钱者B",acct,800).start();
new DepositThread("存钱者C",acct,800).start();
}
}
上述代码便体现了线程使用wait()、notify()、notifyAll()方法进行彼此之间的通信以及控制,防止账户的余额变动异常。
7. ThreadLocal
ThreadLocal类它代表一个线程局部变量。通过将数据放在ThreadLocal<>中就可以为每一个使用该变量的线程创建一份该变量的副本,每个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突,从而避免并发访问的线程安全问题。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48public class ThreadLocalTest extends Thread {
public static void main(String[] args){
Account account = new Account("初始名");
new MyTest(account,"线程A").start();
new MyTest(account,"线程B").start();
}
}
class Account {
//定义一个ThreadLocal类型的变量,该变量是一个线程局部变量
private ThreadLocal<String> name = new ThreadLocal<>();
public Account(String _name) {
this.name.set(_name);
System.out.println("----" + this.name.get());
}
public String getName() {
return this.name.get();
}
public void setName(String _name) {
this.name.set(_name);
}
}
class MyTest extends Thread {
private Account account;
public MyTest(Account _account, String _name) {
super(_name);
this.account = _account;
}
public void run() {
for (int i = 0; i < 10; i++) {
if (i == 3) {
account.setName(getName());
}
System.out.println(account.getName() + "账户的值为:" + i);
}
}
}
—-初始名
null账户的值为:0
null账户的值为:0
null账户的值为:1
null账户的值为:1
null账户的值为:2
null账户的值为:2
线程A账户的值为:3
线程B账户的值为:3
线程A账户的值为:4
线程B账户的值为:4
线程A账户的值为:5
线程B账户的值为:5
线程A账户的值为:6
线程B账户的值为:6
线程A账户的值为:7
线程B账户的值为:7
线程A账户的值为:8
线程B账户的值为:8
线程A账户的值为:9
线程B账户的值为:9
- T get():返回该线程局部变量中当前线程副本的值
- void remove():删除此线程局部变量中当前线程的值
- void set(T value):设置此线程局部变量中当前线程的副本值
- 如果多线程之间需要共享资源,且要避免线程安全问题,应该使用synchronized;如果仅仅是要隔离多线程之间的共享冲突,则可以使用ThreadLocal
本文链接: http://www.xiaopeng.pro/articles/770e889a.html
版权声明: 本原创文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!