目录
- 什么是多线程?引入多线程的意义何在?
- 并行和并发
- 线程安全
- 线程状态
- 如何保证线程安全?
- 创建线程的三种方法
一、什么是多线程?引入多线程的意义何在?
首先介绍一下进程(Process):正在运行中的程序,通常称为进程。(进程拥有自己独立的内存空间,进程与进程之间资源独立)
线程:是最小的处理单元,可认为是进程的子集。(同一进程内的线程共享本进程的地址空间,共享本进程的资源如内存、IO、cpu等,但进程之间资源独立)
多线程:指一个程序(一个进程)运行时产生了不止一个线程;那么引入多线程的原因是什么呢?①资源利用率更好、②程序设计在某些情况下更简单、③程序响应快。
二、并行和并发
- 并行:多个CPU或多台机器同时执行一段处理逻辑,是真正的同时;
- 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。实际上是同一时刻只能有一条指令执行。
三、线程状态
五个基本状态,分别为
- 新建状态(New):当线程对象创建后,即进入新建状态。如Thread r = new MyThread();
- 就绪状态(Runnable):当调用线程对象的start方法,线程就进入了就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待cpu调度执行,并不是说执行了t.start()此线程立即就会执行;
- 运行状态(Running):当cpu开始调度处于就绪状态的线程时,此时线程才得以真正开始执行,即进入到运行状态。注意:就绪状态是进入到运行状态的唯一入口。
- 阻塞状态(Blocked):处于运行中的线程由于某种原因,暂时放弃cpu的使用权,停止执行,此时进入到阻塞状态,直到其进入到就绪状态,才有机会再次被cpu调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1)、等待阻塞:运行状态中的线程执行wait()方法,使本进程进入到等待阻塞状态;
2)、同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它进程占用),它会进入同步阻塞状态;
3)、其它阻塞:通过调用进程的sleep()或join()或发出IO请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待进程终止或者超时、或者IO处理完毕时,线程重新转入就绪状态; - 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束。
一定谨记下面这张经典线程状态图: 线程状态图
上图几个方法的说明:
yield()方法:这是个静态方法,作用是让当前进程“让步”,目的是为了让优先级不低于当前进程的进程有机会运行,这个方法不会释放锁。
join()方法:这是一个实例方法,在当前线程中对一个线程对象调用join方法会导致当前线程停止运行,等那个线程运行完毕后再接着运行当前线程。也就是说,把当前线程还没执行的部分“接到”另一个线程后面去,另一个线程运行完毕后,当前线程再接着运行。
interrupt()方法:这是一个实例方法。每个线程都有一个中断状态标识,这个方法的作用就是将相应线程的中断状态标记为true,这样相应的线程调用isInterrupted方法就会返回true。通过使用这个方法,能够终止那些通过调用可中断方法进入阻塞状态的线程。常见的可中断方法有sleep、wait、join,这些方法的内部实现会时不时的检查当前线程的中断状态,若为true会立刻抛出一个InterruptedException异常,从而终止当前线程。
四、线程安全
线程安全经常来描绘一段代码,指在并发情况下,该代码经过多线程使用,线程的调度顺序不影响任何结果,这个时候使用多线程只需关注内存、cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,比如不加事务的转账代码。
五、如何保证线程安全?
在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。
Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(主要是写),将会导致数据不准确,相互之间产生冲突。加入同步锁可避免该线程没有完成操作之前被其它线程调用,从而保证了该变量的唯一性和正确性。
- 同步方法
即有synchronized关键字修饰的方法。由于Java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
public synchronized void save(){}
synchronized关键字也能修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
- 同步代码块
即有synchronized关键字修饰的语句块。被该关键字修饰的语句块将自动被加上内置锁,从而实现同步。
public class Bank {
private int count =0;//账户余额
//存钱
public void addMoney(int money){
synchronized (this) {
count +=money;
}
System.out.println(System.currentTimeMillis()+"存进:"+money);
}
//取钱
public void subMoney(int money){
synchronized (this) {
if(count-money < 0){
System.out.println("余额不足");
return;
}
count -=money;
}
System.out.println(+System.currentTimeMillis()+"取出:"+money);
}
//查询
public void lookMoney(){
System.out.println("账户余额:"+count);
}
}
注意:同步是一种高开销的操作,因此应该尽量减少同步内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
3.使用特殊域变量(volatile)实现线程同步
1)、volatile关键字为域变量的访问提供了一种免锁机制;
2)、使用volatile修饰域相当于告诉JVM该域可能会被其它线程更新;
3)、因此每次使用该域就要重新计算,而不是使用寄存器中的值;
4)、volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
public class SynchronizedThread {
class Bank {
private volatile int account = 100;
public int getAccount() {
return account;
}
/**
* 用同步方法实现
*
* @param money
*/
public synchronized void save(int money) {
account += money;
}
/**
* 用同步代码块实现
*
* @param money
*/
public void save1(int money) {
synchronized (this) {
account += money;
}
}
}
class NewThread implements Runnable {
private Bank bank;
public NewThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// bank.save1(10);
bank.save(10);
System.out.println(i + "账户余额为:" +bank.getAccount());
}
}
}
/**
* 建立线程,调用内部类
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
System.out.println("线程1");
Thread thread1 = new Thread(new_thread);
thread1.start();
System.out.println("线程2");
Thread thread2 = new Thread(new_thread);
thread2.start();
}
public static void main(String[] args) {
SynchronizedThread st = new SynchronizedThread();
st.useThread();
}
注意:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用final域,有锁保护的域和volatile域可以避免非同步的问题。
4.使用重入锁(Lock)实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
//只给出要修改的代码,其余代码与上同
class Bank {
private int account = 100;
//需要声明这个锁
private Lock lock = new ReentrantLock();
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
lock.lock();
try{
account += money;
}finally{
lock.unlock();
}
}
}
六、创建线程的三种方法
Java中线程的创建常见有三种基本形式
- 继承Thread类,重写run方法
package cn.ihep.thread;
import org.junit.Test;
/**
* 继承Thread类,创建线程
*
* @author xiaoming
*
*/
public class CreateThread {
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println("times :" + i);
}
}
}
@Test
public void test1() {
System.out.println("线程1:");
MyThread t1 = new MyThread();
t1.start();
System.out.println("线程2:");
MyThread t2 = new MyThread();
t2.start();
}
}
注意:run方法才是线程的执行体,start()方法只是告诉cpu老子准备好了,你有空的时候可以来执行我了。
- 实现Runnable接口,并重写该接口的run方法。创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。
package cn.ihep.thread;
import org.junit.Test;
/**
* 通过实现Runnable接口,来创建线程
*
* @author xiaoming
*
*/
public class CreateRunnable {
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println("times = " + i);
}
}
}
@Test
public void test1() {
System.out.println("线程1:");
MyRunnable myRun = new MyRunnable();
Thread t1 = new Thread(myRun);
t1.start();
System.out.println("线程2:");
Thread t2 = new Thread(myRun);
t2.start();
}
}
- 使用Callable和Future接口创建线程。具体步骤:首先创建Callable接口的实现类,并实现call()方法。然后使用Future类包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。
package cn.ihep.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import org.junit.Test;
/**
* 通过Callable和FutureTask来创建线程
*
* @author xiaoming
*
*/
public class CreateThreadByCallable {
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
for (int i = 0; i < 3; i++) {
System.out.println("time = " + i);
}
return null;
}
}
@Test
public void test() {
MyCallable myCa = new MyCallable();
FutureTask<Integer> tf = new FutureTask<>(myCa);
System.out.println("线程1:");
Thread t1 = new Thread(tf);
t1.start();
System.out.println("线程2:");
Thread t2 = new Thread(tf);
t2.start();
// 获取call方法的返回值
try {
Integer res = tf.get();
System.out.println("call方法的返回值为:" + res);
} catch (Exception e) {
e.printStackTrace();
}
}
}
我们发现,在实现Callable接口中,此时不再是run()方法了,而是call()方法,此call()方法作为线程执行体,同时还具有返回值.。 在创建新的线程时,是通过FutureTask来包装MyCallable对象,同时作为了Thread对象的target。
通过看源码,我们还发现FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值
网友评论