多线程
Java基础的第五篇,也是最后一篇-多线程
1.线程的创建和启动
通过集成Thread类创建线程类
- 定义Thread类的子类,并重写run()方法
- 创建Tread子类的实例
- 调用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();
}
}
}
}
运行结果
可以看到一共有三个线程:main Thread0 Thread1 后面两个是新建的。main是程序执行后创建的。
实现Runnable接口创建线程类
- 定义Runnable接口的实现类,并重写run()方法
- 创建Runnable实现类的实例
- 调用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创建线程
- 创建Callable接口实现类,并实现call()方法
- 创建Callable实例使用FutureTask包装
- 使用FutureTask对象作为Thread对象的target创建并启动线程
- 调用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.线程同步
线程安全问题
在这里我们可以用一个经典的问题-银行取钱问题,来进行讲解。
- 用户输入账户密码,系统判断是否正确
- 用户输入取款金额
- 系统判断余额是否大于取款金额
- 大于则取款成功,小于则取款失败
首先定义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();
}
}
启动两个子线程取钱,会出现什么结果呢?
这种结果明显是不对的,这就是我们上面所说的线程同步问题。
之所以出现这样的结果,是因为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() + "取钱失败,余额不足");
}
}
}
再次运行就能得到正确结果:
同步锁(Lock)
Java 5开始,Java提供另一个线程同步机制-通过显示定义同步锁对象实现同步。
通常使用格式如下:
class x
{
public void m(){
lock.lock(); // 加锁
try{
// 需要线程安全的代码
}finally{
lock.unlock();
}
}
}
通过lock和unlock来显示加锁,释放锁。
除了上面所说的知识点,还有线程池,死锁,线程通信等,由于这些知识点都属于高级Java特性,我会在后面的进阶篇再进行总结。