1.线程的创建和启动
继承Thread类
- 定义一个类继承Thread类,重写run()方法,run()方法的方法体就是线程需要完成的任务。run()被称为线程执行体。
- 创建该类的实例,即创建了线程对象。
- 调用线程的start()方法来启动线程。
public class TestDemo01 extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName() + " i=" + i);
}
}
public static void main(String[] args) {
TestDemo01 test = new TestDemo01();
test.start();
}
}
实现Runnable接口
- 定义Runnable接口的实现类,重写run()方法。
- 创建该实现类的实例,然后以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
public class TestDemo02 implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " is start");
}
}
public static void main(String[] args) {
TestDemo02 test = new TestDemo02();
Thread t1 = new Thread(test, "thread1");
Thread t2 = new Thread(test, "thread2");
t1.start();
t2.start();
}
}
使用Callable和Future(Java 5 之后)
Callable接口提供call()作为线程执行体,call()方法比run()方法功能更强大。
- call()方法可以有返回值。
- call()方法可以声明抛出异常。
Java5提供了Future接口代表Callable接口call()方法的返回值。
创建线程的步骤:
- 创建Callable接口的实现类,重写call()方法。
- 创建Callable接口实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动线程。
- 调用FutureTask对象的get()方法来获得子线程结束后的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class TestDemo03 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// TODO Auto-generated method stub
int i = 0;
for (; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " i=" + i);
}
return i;
}
public static void main(String[] args) {
TestDemo03 test = new TestDemo03();
FutureTask<Integer> task = new FutureTask<>(test);
Thread thread = new Thread(task);
thread.start();
try {
System.out.println(task.get());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
三种方式的对比
采用Runnable,Callable接口实现线程:
- 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
- 这种方式下,多个线程可以用同一个target,非常适合多个相同线程来处理同一份资源的情况。
- 编程稍微复杂,要访问当前线程,必须使用Thread.currentThread()方法。
采用继承Thread类实现线程:
- 编写简单,要访问当前线程,用this关键字就行。
- 线程类已经继承了Thread类,不能再继承其他类。
一般使用接口的方式来创建多线程。
2.线程的生命周期
线程一共有5种状态:新建(New),就绪(Runnable),运行(Running),阻塞(Blocked),死亡(Dead)。
程序使用new关键字创建了一个线程之后,该线程处于新建状态。
线程对象调用了start()方法之后,线程处于就绪状态。
如果处于就绪状态的线程获得了CPU,开始执行run()方法的方法体,线程处于运行状态。
发生如下情况,线程会进入阻塞状态:
- 线程调用sleep()方法主动放弃占用的处理器资源。
- 线程调用了一个阻塞式IO方法,在该方法返回之前,线程被阻塞。
- 线程试图获得一个同步监视器,但该同步监视器正在被其他线程调用。
- 线程在等待某个通知(notify).
- 程序调用了线程的suspend()方法将该线程挂起(该方法容易导致死锁,应该尽量避免使用).
阻塞清清除后,线程进入就绪状态:
- 调用sleep()方法的线程过了指定时间。
- 线程调用的阻塞式IO方法已经返回。
- 线程获得了同步监视器。
- 线程正在等待通知时,其他线程发出了一个通知。
- 处于挂起状态的线程被调用了resume()恢复方法。
线程会以如下方法结束,结束后处于死亡状态:
- run()方法或call()方法执行完成,线程正常结束。
- 线程抛出一个未捕获的Exception或Error。
- 直接调用该线程的stop()方法来结束线程(该方法容易导致死锁,一般不推荐使用)。
死亡之后的线程不可以再次调用start()方法。
判断线程是否死亡可以调用isAlive()方法,新建和死亡状态会返回false,其他返回true。
3.控制线程
join线程
join()方法:让一个线程等待另一个线程完成。
在一个线程中调用其他线程的join()方法,该线程会阻塞,等到join()加入的线程执行完成后才会解除阻塞。
public class TestDemo04 implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " i=" + i);
}
}
public static void main(String[] args) {
Thread t = new Thread(new TestDemo04());
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " i=" + i);
if (i == 5) {
t.start();
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
后台线程
该线程是在后台运行的,任务是为其他线程提供服务。
如果所有前台线程死亡,后台线程会自动死亡。
调用Thread的setDaemon(true)方法可以将指定的线程设置成后台线程。
线程睡眠:sleep
sleep()方法:让当前线程休眠一段时间,进入阻塞状态。
import java.util.Date;
public class TestDemo05 implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(new Date());
}
}
public static void main(String[] args) {
new Thread(new TestDemo05()).start();
}
}
线程让步:yield
yield():让当前线程暂停,但不会阻塞该线程,而是转成就绪状态。
某个线程调用yield()方法暂停后,只有优先级大于或等于当前线程且处于就绪状态的线程才可以获得执行的机会。
sleep()方法和yield()方法的区别:
- sleep()方法暂停线程后会给其他所有线程执行机会,yield()方法暂停线程后只会给优先级大于或等于自己的线程执行机会。
- sleep()方法会把线程转入阻塞状态,时间到了之后才会转成就绪状态;yield()方法强制把线程转入就绪状态。
- sleep()方法抛出InterruptedException异常,yield()方法没有抛出异常。
- sleep()方法比yield()方法有更好的移植性。(一般不建议用yield()方法控制并发线程的执行)。
4.线程同步
线程安全问题
package com.lwcode.thread;
public class TestDemo06 {
public static void main(String[] args) {
BankAccount account = new BankAccount(1000);
Customer c1 = new Customer(account);
Customer c2 = new Customer(account);
Thread t1 = new Thread(c1, "甲");
Thread t2 = new Thread(c2, "乙");
t1.start();
t2.start();
}
}
class Customer implements Runnable {
private BankAccount account = null;
public Customer(BankAccount account) {
// TODO Auto-generated constructor stub
this.account = account;
}
@Override
public void run() {
// TODO Auto-generated method stub
while (true)
account.getMoney(200);
}
}
class BankAccount {
private int money;
public BankAccount(int money) {
this.money = money;
}
public void getMoney(int num) {
if (money >= num) {
money -= num;
System.out.println(Thread.currentThread().getName() + "取钱" + num + ",余额:" + money);
} else {
System.out.println("余额不足");
System.exit(0);
}
}
}
该代码就会出现线程安全问题,原因是CPU切换线程的不确定性。
解决方法
1) 同步代码块
使用同步监视器的通用方法就是同步代码块,使用synchronized关键字;
synchronized(obj){//obj就是同步监视器
//code
}
线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。任何时刻只有一个线程可以获得对同步监视器的锁定,当同步代码执行完成后,该线程释放对同步监视器的锁定。
利用同步代码块修改后的代码如下:
public void run() {
// TODO Auto-generated method stub
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (account) {
account.getMoney(200);
}
}
}
2)同步方法
同步方法就是使用synchronized关键字修饰该方法。对于同步方法,不需要指定同步监视器,同步方法同步监视this,也就是该对象本身。
通过使用同步方法方便实现线程安全的类:
- 该类的对象可以被多个线程安全访问。
- 每个线程调用该对象的任意方法后都得到正确的结果。
- 每个线程调用该对象的任意方法后,该对象状态依然保持合理状态。
synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器,属性。
public synchronized void getMoney(int num) {
if (money >= num) {
money -= num;
System.out.println(Thread.currentThread().getName() + "取钱" + num + ",余额:" + money);
} else {
System.out.println("余额不足");
System.exit(0);
}
}
3) 同步锁
Java 5 开始显式定义同步锁对象来实现同步,同步锁使用Lock对象。
实现线程安全控制中,比较常用的是ReentrantLock(可重入锁),使用该Lock对象可以显式的加锁、释放锁。
class TestLock {
// 定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// 定义需要保证线程安全的方法
public void method() {
// 加锁
lock.lock();
try {
// 需要保证线程安全的代码
} finally {
lock.unlock();
}
}
}
使用同步锁后的代码如下:
class BankAccount {
private int money;
private final ReentrantLock lock = new ReentrantLock();
public BankAccount(int money) {
this.money = money;
}
public void getMoney(int num) {
lock.lock();
try {
if (money >= num) {
money -= num;
System.out.println(Thread.currentThread().getName() + "取钱" + num + ",余额:" + money);
} else {
System.out.println("余额不足");
System.exit(0);
}
} finally {
lock.unlock();
}
}
}
死锁
当两个线程互相等对方释放同步监视器时就会发生死锁,Java虚拟机没有采取措施来处理死锁情况,所以应该避免出现死锁。
package com.lwcode.thread;
public class TestDemo07 implements Runnable {
public static void main(String[] args) {
TestDemo07 testDemo07 = new TestDemo07();
new Thread(testDemo07).start();
testDemo07.init();
}
A a = new A();
B b = new B();
@Override
public void run() {
// TODO Auto-generated method stub
Thread.currentThread().setName("B线程");
b.first(a);
System.out.println("进入B线程后");
}
public void init() {
Thread.currentThread().setName("A线程");
a.first(b);
System.out.println("进入A线程后");
}
}
class A {
public synchronized void first(B b) {
System.out.println(Thread.currentThread().getName() + "进入了A类first方法");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "试图调用B类的last方法");
b.last();
}
public synchronized void last() {
System.out.println("A类的last方法");
}
}
class B {
public synchronized void first(A a) {
System.out.println(Thread.currentThread().getName() + "进入了B类first方法");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "试图调用A类的last方法");
a.last();
}
public synchronized void last() {
System.out.println("B类的last方法");
}
}
由图可以看出,程序并没有结束,但是不会向下运行,这就是产生了死锁。