目录
java是支持多线程的。
进程和线程
进程:进程是为了能使多个程序能够并发执行,提高资源的利用率和系统的吞吐量而存在的。进程是CPU、内存等资源分配的基本单位。在日常使用中,QQ就可以看做一个进程。
线程:操作系统在进行进程间切换的时候,付出的开销较大(因为需要暂存当前进程控制块环境和设置新选中的进程的CPU环境)。线程做为进程中一个相对独立的,可调度的执行单元,在操作线程时,操作系统付出的开销比较小。线程是程序执行时的最小单位。我们可以将QQ的接收好友消息、提示空间点赞等功能看作是多个线程。
多线程的实现
单线程执行多个任务
由于只有一个线程,只能按顺序一个一个执行任务:
public class SingleThreadingTestApp {
public static void main(String[] args) {
System.out.println("任务1");
System.out.println("任务2");
System.out.println("任务3");
System.out.println("任务4");
}
}
多线程执行多个任务
由于有多个线程,可以将任务分配给多个线程来完成,顺便来讲一讲线程类的创建:
一、通过继承Thread类来创建自定义线程类
1.自定义一个类继承Thread类
2.重写run()方法
public class MyThread1 extends Thread{
@Override
public void run() {
System.out.println("任务1");
}
}
二、通过实现Runnable接口来创建自定义线程任务类
1.自定义一个类实现Runable接口
2.重写run方法
public class MyThread2 implements Runnable{
@Override
public void run() {
System.out.println("任务2");
}
}
三、通过实现Callable接口来实现自定义线程任务类
1.自定义一个类实现Callable接口
2.重写call()方法
public class MyThread3 implements Callable {
@Override
public Object call() throws Exception {
System.out.println("任务3");
return null;
}
}
四、创建线程对象,并执行任务
可以看出来不管创建线程任务类用的是什么方法,最后都是通过Thread来创建运行的。
public class MultiThreadingTestApp {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//通过继承Thread类来创建一个线程对象
Thread thread1 = new MyThread1();
//通过实现Runnable接口来创建一个线程对象
Thread thread2 = new Thread(new MyThread2());
//通过实现Callable接口来创建一个线程对象
FutureTask futureTask = new FutureTask(new MyThread3());
Thread thread3 = new Thread(futureTask);
//启动线程
thread1.start();
thread2.start();
thread3.start();
System.out.println("任务4");
}
}
线程的运行顺序也不是安照创建或者启动线程的顺序执行的。因为java使用的是抢占式线程调度模型,将CPU时间分为许多时间片,由每个线程去抢CPU时间片,抢到CPU时间片的线程就可以运行,当时间片使用完或者线程执行完成后线程会让出CPU使用权,线程继续抢时间片。还有一种调度模型是均分式调度模型,特点是平均分配CPU时间片给每个线程。
五、三种方式的对比
由于Java只支持单继承,如果自定义的线程类需要继承其它类,则使用实现Runnale接口和实现Callable接口的方式比继承Thread的方式更加灵活。实现Callable接口的方式中,call()方法是有返回值的,在启动线程的方法中可以使用Object obj = futureTask.get()来获取返回值,实现线程间的通信。线程间的通信我们也可以用其它方式(如修改成员变量等)来实现。
线程的生命周期
1.新建状态:当线程被new出来时就处于新建状态。
2.就绪状态:调用线程对象的start()方法使线程进入就绪状态,就绪状态下的线程可以抢夺CPU时间片。
3.运行状态:抢夺到CPU时间片的线程进入运行状态,执行run()方法,当CPU时间片用完或者run()方法执行完成,让出时间片。如果run()方法没有执行完成就用完了时间片,则该线程会继续抢夺时间片,如果抢夺到时间片,接着上次结束的代码继续运行。
4.阻塞状态:运行状态下的线程遇到用户键盘输入、执行线程类的sleep()方法等阻塞事件就会让出时间片,进入阻塞状态。阻塞状态结束后进入就绪状态继续抢夺时间片。
5.死亡状态:当run()方法执行完成,线程死亡。
线程安全问题
一、数据安全性问题
当多个线程同时修改一个变量时就会产生数据安全问题。
如一个账户有1000元,A和B同时去取1000元,这会创建两个线程,两个线程都需要读取当前余额,进行-1000操作,然后重新赋值回账户,执行期间可能会产生两个账户同时取出了1000元的问题。我们来模拟这个问题的产生:
创建一个账户类
账户中有1000元。
public class Account {
int balance = 1000;
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
创建线程任务类
任务是从账户中取出1000元,我们使用Thread.sleep()方法来模拟网络延迟,这样在本例中更容易产生数据安全问题。Thread.currentThread()方法是获取当前执行的线程,使用当前线程.getName()方便区分两个线程。
public class Withdraw implements Runnable{
Account account;
public Withdraw() {
}
public Withdraw(Account account) {
this.account = account;
}
@Override
public void run() {
int balance = account.getBalance();
if (balance >= 1000){
System.out.println(Thread.currentThread().getName() + "取出了1000元,当前账户还剩" + (balance - 1000) + "元");
}else{
System.out.println("您的余额不足1000元无法取钱");
}
balance -= 1000;
//线程睡眠1秒模拟网络延迟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.setBalance(balance);
}
}
模拟同时取钱操作
public class ThreadSafetyIssues {
public static void main(String[] args) {
Account account = new Account();
//A和B共享一个账户
Withdraw A = new Withdraw(account);
Withdraw B = new Withdraw(account);
Thread t1 = new Thread(A);
Thread t2 = new Thread(B);
//给线程起名方便辨认
t1.setName("A");
t2.setName("B");
//同时进行取钱操作
t1.start();
t2.start();
}
}
执行结果,同一个账户被连续两次取了1000元,但是账户只扣除了1000元。出现了数据安全问题。
解决方法
使用synchronized关键字来解决数据安全性问题。synchronized表示同步的,它可以用来修饰方法,也可以修饰代码块。我们可以认为每个对象都有一把锁,被synchronized括起来的代码块需要我们获取到共享对象的锁只后才能运行。
synchronized(共享对象){
代码块
}
被synchronized修饰的方法就相当于将整个方法体放到
synchronized(this){
方法体
}
中,this是调用方法的对象。
如果静态方法使用synchronized修饰就是为这个方法添加类锁。
我们可以使用synchronized来修改之前的线程任务类来解决数据安全性问题。修改后的类如下
public class Withdraw implements Runnable{
Account account;
public Withdraw() {
}
public Withdraw(Account account) {
this.account = account;
}
@Override
public void run() {
synchronized (account) {
int balance = account.getBalance();
if (balance >= 1000) {
System.out.println(Thread.currentThread().getName() + "取出了1000元,当前账户还剩" + (balance - 1000) + "元");
} else {
System.out.println(Thread.currentThread().getName() + "您的余额不足1000元无法取钱");
}
balance -= 1000;
//线程睡眠1秒模拟网络延迟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.setBalance(balance);
}
}
}
执行结果
二、死锁问题
死锁就是t1线程获取到了对象A的锁,希望获取对象B的锁,t2线程获取到了对象B的锁希望获取对象A的锁,两个线程都在等待对方释放锁,自己不释放锁,就形成了死锁。
死锁的四个必要条件:互斥条件、请求和保持条件、不剥夺条件、环路等待条件。
我们来模拟死锁的产生
创建两个类A和B
简便起见,我们不给这两个类添加属性和方法,它们都只有一个默认构造器。
public class A {
}
public class B {
}
创建两个线程任务类
public class DeadlockTestThread1 implements Runnable{
A a;
B b;
public DeadlockTestThread1(A a, B b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
String thisName = Thread.currentThread().getName();
synchronized (a){
System.out.println(thisName + "获取到了对象a的锁,等待获取对象b的锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println(thisName + "获取到对象b的锁执行业务代码");
}
System.out.println(thisName + "释放对象b的锁");
}
System.out.println(thisName + "释放对象a的锁");
}
}
public class DeadlockTestThread2 implements Runnable{
A a;
B b;
public DeadlockTestThread2(A a, B b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
String thisName = Thread.currentThread().getName();
synchronized (b){
System.out.println(thisName + "获取到了对象b的锁,等待获取对象b的锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a){
System.out.println(thisName + "获取到对象a的锁执行业务代码");
}
System.out.println(thisName + "释放对象a的锁");
}
System.out.println(thisName + "释放对象b的锁");
}
}
两个类的run()方法基本是一样的,就是获取锁的顺序不一样。
测试示例
public class DeadlockDemo {
public static void main(String[] args) {
A a = new A();
B b = new B();
Thread t1 = new Thread(new DeadlockTestThread1(a,b));
Thread t2 = new Thread(new DeadlockTestThread2(a,b));
t1.start();
t2.start();
}
}
运行结果,两个线程获取到第一个需要的对象锁后就相互等待对方释放锁,进入死锁状态。
解决方法
1.规定t1和t2线程都先获取对象a的锁再去获取对象b的锁或者都先获取对象b的锁再去获取对象a的锁,破环环路等待条件。
2.破环请求和保持条件。在对象中设置标志位,如果对象的锁被获取了,就设置标志位,其它线程获取对象锁之前先查看标志位,如果对象锁已经被获取就等待,等待一段时间后如果对象锁还没有被释放就释放自己已经获取的对象锁。
方法1很简单,我们来简单模拟一下方法2:
为A、B对象添加标志位
public class A {
boolean lock = false;
public boolean isLock() {
return lock;
}
public void setLock(boolean lock) {
this.lock = lock;
}
public void toggleLock(){
this.lock = !lock;
}
}
public class B {
boolean lock = false;
public boolean isLock() {
return lock;
}
public void setLock(boolean lock) {
this.lock = lock;
}
public void toggleLock(){
this.lock = !lock;
}
}
修改线程任务类,添加循环等待和释放锁的条件
public class DeadlockTestThread1 implements Runnable{
//最大等待次数
final static int MAX_WAIT_COUNT = 3;
//每次等待范围的扩大倍数
final static int WAIT_RANGE_MAGNIFICATION = 2;
A a;
B b;
//重新等待的范围,用于随机一个等待时间
int waitRange = 1;
Random random = new Random();
public DeadlockTestThread1(A a, B b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
String thisName = Thread.currentThread().getName();
//标志是否完成业务
boolean couldReturn = false;
while (true) {
int count = MAX_WAIT_COUNT;
while (count > 0) {
count--;
//对象a锁没被获取就获取对象a锁
if (!a.isLock()) {
synchronized (a) {
//重置等待范围
waitRange = 1;
//修改标志位,告诉其它线程对象a锁已经被获取了
a.toggleLock();
System.out.println(thisName + "获取到了对象a锁,等待获取对象b锁");
/* ****************************获取对象b锁start**************************************** */
int count2 = MAX_WAIT_COUNT;
while (count2 > 0) {
count2--;
//对象b锁没被获取就获取对象b锁
if (!b.isLock()) {
synchronized (b) {
//修改标志位,告诉其它线程对象b锁已经被获取了
b.toggleLock();
System.out.println(thisName + "获取到了对象b锁,执行业务代码");
couldReturn = true;
}
System.out.println(thisName + "释放对象b锁");
//修改标志位,告诉其它线程对象b锁已经被释放了
b.toggleLock();
break;
}
if (count2 > 0) {
System.out.println(thisName + "获取对象b锁失败,等待再次获取对象b锁");
}
//等待一会后再去获取对象b锁
try {
Thread.sleep(random.nextInt(waitRange) * 100);
} catch (InterruptedException e) {
e.printStackTrace();
}
waitRange *= WAIT_RANGE_MAGNIFICATION;
}
/* ****************************获取对象b锁end**************************************** */
}
System.out.println(thisName + "释放对象a锁");
//修改标志位,告诉其它线程对象a锁已经被释放了
a.toggleLock();
if (couldReturn) {
waitRange = 1;
return;
}
}
if (count > 0) {
System.out.println(thisName + "获取对象a锁失败,等待再次获取对象a锁");
}
//等待一会后再去获取对象a锁
try {
Thread.sleep(random.nextInt(waitRange) * 100);
} catch (InterruptedException e) {
e.printStackTrace();
}
waitRange *= WAIT_RANGE_MAGNIFICATION;
}
}
}
}
public class DeadlockTestThread2 implements Runnable{
//最大等待次数
final static int MAX_WAIT_COUNT = 3;
//每次等待范围的扩大倍数
final static int WAIT_RANGE_MAGNIFICATION = 2;
A a;
B b;
//重新等待的范围,用于随机一个等待时间
int waitRange = 1;
Random random = new Random();
public DeadlockTestThread2(A a, B b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
String thisName = Thread.currentThread().getName();
//标志是否完成业务
boolean couldReturn = false;
while (true) {
int count = MAX_WAIT_COUNT;
while (count > 0) {
count--;
//对象b锁没被获取就获取对象b锁
if (!b.isLock()) {
synchronized (b) {
//重置等待范围
waitRange = 1;
//修改标志位,告诉其它线程对象b锁已经被获取了
b.toggleLock();
System.out.println(thisName + "获取到了对象b锁,等待获取对象a锁");
/* ****************************获取对象a锁start**************************************** */
int count2 = MAX_WAIT_COUNT;
while (count2 > 0) {
count2--;
//对象a锁没被获取就获取对象a锁
if (!a.isLock()) {
synchronized (a) {
//修改标志位,告诉其它线程对象a锁已经被获取了
a.toggleLock();
System.out.println(thisName + "获取到了对象a锁,执行业务代码");
couldReturn = true;
}
System.out.println(thisName + "释放对象a锁");
//修改标志位,告诉其它线程对象a锁已经被释放了
a.toggleLock();
break;
}
if (count2 > 0) {
System.out.println(thisName + "获取对象a锁失败,等待再次获取对象a锁");
}
//等待一会后再去获取对象a锁
try {
Thread.sleep(random.nextInt(waitRange) * 100);
} catch (InterruptedException e) {
e.printStackTrace();
}
waitRange *= WAIT_RANGE_MAGNIFICATION;
}
/* ****************************获取对象a锁end**************************************** */
}
System.out.println(thisName + "释放对象b锁");
//修改标志位,告诉其它线程对象b锁已经被释放了
b.toggleLock();
if (couldReturn) {
waitRange = 1;
return;
}
}
if (count > 0) {
System.out.println(thisName + "获取对象b锁失败,等待再次获取对象b锁");
}
//等待一会后再去获取对象b锁
try {
Thread.sleep(random.nextInt(waitRange) * 100);
} catch (InterruptedException e) {
e.printStackTrace();
}
waitRange *= WAIT_RANGE_MAGNIFICATION;
}
}
}
}
测试用例
public class DeadlockDemo {
public static void main(String[] args) {
A a = new A();
B b = new B();
Thread t1 = new Thread(new DeadlockTestThread1(a,b));
Thread t2 = new Thread(new DeadlockTestThread2(a,b));
t1.start();
t2.start();
}
}
查看运行结果
能够解决死锁问题。