Java基础-多线程

多线程

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

1.线程的创建和启动

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

举个例子:

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()方法启动线程

举个例子:

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()方法获得返回值

举个例子:

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类,具有账户名和余额两个属性

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;
    }
}

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

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() + "取钱失败,余额不足");
        }
    }
}

最后还有主程序:

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多线程引入了同步监视器。语法如下:

synchronized(obj) 
{ 
    // 同步代码块
} 

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

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提供另一个线程同步机制-通过显示定义同步锁对象实现同步。

通常使用格式如下:

class x 
{ 
    public void m(){
        lock.lock(); // 加锁
        try{
            // 需要线程安全的代码
        }finally{
            lock.unlock();
        }
    }
} 

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值