Java基础的第五篇,也是最后一篇-多线程
多线程
1.线程的创建和启动
通过集成Thread类创建线程类
- 定义Thread类的子类,并重写run()方法
- 创建Tread子类的实例
- 调用start()方法启动线程
举个例子:
1 | public class FirstThread extends Thread { |
运行结果
可以看到一共有三个线程:main Thread0 Thread1 后面两个是新建的。main是程序执行后创建的。
实现Runnable接口创建线程类
- 定义Runnable接口的实现类,并重写run()方法
- 创建Runnable实现类的实例
- 调用start()方法启动线程
举个例子:
1 | public class SecondThread implements Runnable{ |
运行结果和FirstThread类似,就不详细描述了。
这里有一点区别:FirstThread里面新建Thread是可以直接调用start()方法,因为是Tread的子类,但是Runnable里面只是线程对象的target,不能直接调用runnable.start()
使用Callable和Future创建线程
- 创建Callable接口实现类,并实现call()方法
- 创建Callable实例使用FutureTask包装
- 使用FutureTask对象作为Thread对象的target创建并启动线程
- 调用FutureTask的get()方法获得返回值
举个例子:
1 | public class ThirdThread implements Callable<Integer> { |
运行结果和前面的类似,不过最后会输出call()方法的返回值
说完了Thread,Runnable,Callable三种创建线程的方式,我们来比较一下
采用Runnable、Callable接口的方式:
- 线程只是实现接口,还可以继承其他类
- 多个线程可以共享一个target对象,适合多个相同线程处理同一份资源的情况
- 劣势:需要使用Thread.currentThread()方法访问当前进程
采用Thread的优势正好是上面两种方法的劣势。
2.线程的生命周期
新建和就绪状态
使用new关键字创建对象就处于新建状态,使用start()方法之后就处于就绪状态,至于什么时候开始执行,要看JVM的调度。
运行和阻塞状态
调用了sleep()方法,调用了一个阻塞式IO方法,等待某个通知…都会让线程阻塞
相对应的就是运行状态,这一块知识点有点像操作系统的CPU轮换。
线程死亡
- run()或call()方法执行完成,线程正常结束
- 线程抛出未捕获的异常
- 直接调用stop()
这三种情况都会让线程结束
3.线程同步
线程安全问题
在这里我们可以用一个经典的问题-银行取钱问题,来进行讲解。
- 用户输入账户密码,系统判断是否正确
- 用户输入取款金额
- 系统判断余额是否大于取款金额
- 大于则取款成功,小于则取款失败
首先定义Account类,具有账户名和余额两个属性
1 | public class Account { |
然后定义一个取钱的线程类
1 | public class DrawThread extends Thread{ |
最后还有主程序:
1 | public class DrawTest { |
启动两个子线程取钱,会出现什么结果呢?
这种结果明显是不对的,这就是我们上面所说的线程同步问题。
之所以出现这样的结果,是因为run()方法不具有同步安全性,一旦程序并发修改Account对象,就很容易出现这种错误结果。
为了解决这个问题,Java多线程引入了同步监视器。语法如下:
1 | synchronized(obj) |
我们再修改一下DrawThread的代码:
1 | public void run(){ |
再次运行就能得到正确结果:
同步锁(Lock)
Java 5开始,Java提供另一个线程同步机制-通过显示定义同步锁对象实现同步。
通常使用格式如下:
1 | class x |
通过lock和unlock来显示加锁,释放锁。
除了上面所说的知识点,还有线程池,死锁,线程通信等,由于这些知识点都属于高级Java特性,我会在后面的进阶篇再进行总结。