Java基础-多线程

Java基础的第五篇,也是最后一篇-多线程

多线程

1.线程的创建和启动

通过集成Thread类创建线程类

  1. 定义Thread类的子类,并重写run()方法
  2. 创建Tread子类的实例
  3. 调用start()方法启动线程

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class FirstThread extends Thread {

private int i;
public void run(){
for (;i<100;i++)
{
System.out.println(getName() + " " + i);
}
}

public static void main(String[] args) {
for (int i=0; i<100; i++)
{
System.out.println(Thread.currentThread().getName());
if (i == 20)
{
// 创建并启动第一个线程
new FirstThread().start();
// 创建并启动第二个线程
new FirstThread().start();
}
}
}
}

运行结果

img

可以看到一共有三个线程:main Thread0 Thread1 后面两个是新建的。main是程序执行后创建的。

实现Runnable接口创建线程类

  1. 定义Runnable接口的实现类,并重写run()方法
  2. 创建Runnable实现类的实例
  3. 调用start()方法启动线程

举个例子:

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
public class SecondThread implements Runnable{

private int i;

@Override
public void run() {
for (; i<100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
}
}

public static void main(String[] args) {
for (int i=0; i<100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
SecondThread st = new SecondThread();
new Thread(st,"新线程1").start();
new Thread(st,"新线程2").start();
}
}
}
}

运行结果和FirstThread类似,就不详细描述了。

这里有一点区别:FirstThread里面新建Thread是可以直接调用start()方法,因为是Tread的子类,但是Runnable里面只是线程对象的target,不能直接调用runnable.start()

使用Callable和Future创建线程

  1. 创建Callable接口实现类,并实现call()方法
  2. 创建Callable实例使用FutureTask包装
  3. 使用FutureTask对象作为Thread对象的target创建并启动线程
  4. 调用FutureTask的get()方法获得返回值

举个例子:

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
public class ThirdThread implements Callable<Integer> {

@Override
public Integer call(){

int i=0;
for (;i<100;i++){
System.out.println(Thread.currentThread().getName() + " " + i);
}
return i;
}

public static void main(String[] args) {

ThirdThread rt = new ThirdThread();

FutureTask<Integer> task = new FutureTask<>(rt);

for (int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20){
new Thread(task,"有返回值的线程").start();
}
}
try {
System.out.println("子线程返回值:" + task.get());
}catch (Exception ex){
ex.printStackTrace();
}
}
}

运行结果和前面的类似,不过最后会输出call()方法的返回值

说完了Thread,Runnable,Callable三种创建线程的方式,我们来比较一下

采用Runnable、Callable接口的方式:

  • 线程只是实现接口,还可以继承其他类
  • 多个线程可以共享一个target对象,适合多个相同线程处理同一份资源的情况
  • 劣势:需要使用Thread.currentThread()方法访问当前进程

采用Thread的优势正好是上面两种方法的劣势。

2.线程的生命周期

新建和就绪状态

使用new关键字创建对象就处于新建状态,使用start()方法之后就处于就绪状态,至于什么时候开始执行,要看JVM的调度。

运行和阻塞状态

调用了sleep()方法,调用了一个阻塞式IO方法,等待某个通知…都会让线程阻塞

相对应的就是运行状态,这一块知识点有点像操作系统的CPU轮换。

线程死亡

  • run()或call()方法执行完成,线程正常结束
  • 线程抛出未捕获的异常
  • 直接调用stop()

这三种情况都会让线程结束

3.线程同步

线程安全问题

在这里我们可以用一个经典的问题-银行取钱问题,来进行讲解。

  1. 用户输入账户密码,系统判断是否正确
  2. 用户输入取款金额
  3. 系统判断余额是否大于取款金额
  4. 大于则取款成功,小于则取款失败

首先定义Account类,具有账户名和余额两个属性

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
public class Account {

private String accountNo;

private double balance;

public Account(){}

public Account(String accountNo,double balance){
this.accountNo = accountNo;
this.balance = balance;
}

public int hashCode(){

return accountNo.hashCode();

}

public String getAccountNo() {
return accountNo;
}

public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}

public double getBalance() {
return balance;
}

public void setBalance(double balance) {
this.balance = balance;
}

public boolean equals(Object obj){
if (this == obj){
return true;
}
if (obj != null && obj.getClass() == Account.class){
Account target = (Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

然后定义一个取钱的线程类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DrawThread extends Thread{

private Account account;

private double drawAmount;

public DrawThread(String name, Account account, double drawAmount){
super(name);
this.account = account;
this.drawAmount = drawAmount;
}

public void run(){
if (account.getBalance() >= drawAmount){
System.out.println("取钱成功:" + drawAmount);
account.setBalance(account.getBalance() - drawAmount);
System.out.println("余额为:" + account.getBalance());
}else{
System.out.println(getName() + "取钱失败,余额不足");
}
}
}

最后还有主程序:

1
2
3
4
5
6
7
public class DrawTest {
public static void main(String[] args) {
Account acct = new Account("1234567",1000);
new DrawThread("甲",acct,800).start();
new DrawThread("乙",acct,800).start();
}
}

启动两个子线程取钱,会出现什么结果呢?

img

这种结果明显是不对的,这就是我们上面所说的线程同步问题。

之所以出现这样的结果,是因为run()方法不具有同步安全性,一旦程序并发修改Account对象,就很容易出现这种错误结果。

为了解决这个问题,Java多线程引入了同步监视器。语法如下:

1
2
3
4
synchronized(obj) 
{
// 同步代码块
}

我们再修改一下DrawThread的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void run(){
// 使用account作为同步监视器,任何进程进入以下同步代码块之前
// 必须先获得对account账户的锁定- 其他县城无法获得锁,也就无法修改它
// 这种做法符合 加锁-修改-释放 的逻辑
synchronized (account) {
if (account.getBalance() >= drawAmount) {
System.out.println("取钱成功:" + drawAmount);
account.setBalance(account.getBalance() - drawAmount);
System.out.println("余额为:" + account.getBalance());
} else {
System.out.println(getName() + "取钱失败,余额不足");
}
}
}

再次运行就能得到正确结果:

img

同步锁(Lock)

Java 5开始,Java提供另一个线程同步机制-通过显示定义同步锁对象实现同步。

通常使用格式如下:

1
2
3
4
5
6
7
8
9
10
11
class x 
{
public void m(){
lock.lock(); // 加锁
try{
// 需要线程安全的代码
}finally{
lock.unlock();
}
}
}

通过lock和unlock来显示加锁,释放锁。

除了上面所说的知识点,还有线程池,死锁,线程通信等,由于这些知识点都属于高级Java特性,我会在后面的进阶篇再进行总结。


本文结束啦感谢您的阅读