不忘初心 砥砺前行, Tomorrow Is Another Day !
相关文章
本文概要:
- 进程与线程
- 线程的生命周期
- 线程易混淆的函数
- 锁机制
- 线程同步的四种方式
- 对 volatile 关键字的理解
一. 进程和线程
1.1 基本概念
- 进程
概念:进程是程序的实体,是受操作系统管理的基本运行单元.
- 线程
概念:操作系统中最小调度单元,一个进程可以拥有多个线程.
1.2 线程的生命周期
- new : 新建状态,当new Thread实例化时.
- Runnable : 可运行状态,当调用start方法时.
- Running : 运行状态,线程被cpu执行,调用了run方法时.
- Blocked 阻塞状态,当调用join()、sleep()、wait()时.分三种阻塞情况
- wait : 等待状态,当调用wait方法时,此时需要调用它的notify方法去唤醒它,才会重回可运行状态.
- timeWait : 超时等待状态,当调用t.join(long)、Thread.sleep(long),obj.wait(long)时,超过指定时间,都会自动返回可运行状态.
- lock: 同步状态,获取对象的同步锁,若该同步锁被别的线程占用时.
- Dead : 销毁状态,线程执行完毕或者发生异常时.
1.3 线程易混淆的函数
- Thread.sleep()/sleep(long millis),当前线程进阻塞状态,不会释放锁
- t.join()/join(long millis),在当前线程里调用其它线程的join方法,当前线程进阻,不放锁.
- (相当于其他线程插队进来,需要等待其他线程执行完毕才可往下执行),
- obj.wait()/wait(long timeout),当前线程进阻,放锁.需要依靠notify()/notifyAll()唤醒或者等待时间到自动唤醒
- obj.notify()/obj.nofiyAll唤醒在此对象监视器上阻塞的任意某个线程/所有线程.
- Thread.yield(),当前线程不进阻,不放锁.而是重置为可运行状态.
- interrupt(),中断线程.
二. 线程的创建
线程的创建有3种方式.
2.1 继承Thread,重写run方法.
这种方式的本质也是实现Runnable接口.当我们调用start方法时,并不会立即执行线程里面代码,而只是将线程状态变为可执行状态,具体的执行时机由操作系统决定.
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("直接继承Thread,重写run方法");
}
public static void main(String[] args) {
new MyThread().start();
}
}
2.2 实现Runnable接口,重写run方法.
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable接口,重写run方法");
}
public static void main(String[] args) {
new Thread(new MyRunnable()).start();
}
}
2.3 实现Callable接口,重写Call方法.
相比Runnable的三大功能.
- 可以抛出异常
- 提供返回值
- 通过Future异步任务统计,可以对目标线程Call方法监视执行情况,获取执行完毕时的返回值结果.
关于ExecutorService与Future相关知识,将在线程池一篇文中详细讲解.这里只需要知道Callable一般是和ExecutorService配合来使用的.
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("子线程正在干活");
Thread.sleep(3000);
return "实现Callable接口,重写Call方法";
}
public static void main(String[] args) throws Exception {
MyCallable myCallable = new MyCallable();
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(myCallable);
executorService.shutdown();
Thread.sleep(1000);//模拟正在干活
System.out.println("主线程正在干活");
//阻塞当前线程,等待返回结果.
System.out.println("等待返回结果:" + future.get());
System.out.println("主线程所有的活都干完了");
}
}
//调用输出
子线程正在干活
主线程正在干活
等待返回结果:实现Callable接口,重写Call方法
主线程所有的活都干完了
三. 线程的同步
线程同步的目的就是为了防止当多个线程对同一个数据对象进行存储时,造成数据不一致的问题.
同步问题示例
public class SyncThread extends Thread {
private static final String TAG = "SyncThread";
private Pay pay;
public SyncThread(String name, Pay pay) {
super(name);
this.pay = pay;
}
@Override
public void run() {
while (isRunning) {
pay.count();
}
}
}
Pay.java
/**
* 未同步时
*/
public void count() {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + ":>" + count--);
} else {
isRunning = false;
}
}
//未使用同步时
D: 线程3:>912
D: 线程1:>911
D: 线程2:>911 //此时已经出现数据不一样
D: 线程1:>909
D: 线程3:>910
D: 线程2:>908
D: 线程1:>907
D: 线程3:>906
D: 线程2:>905
D: 线程1:>904
D: 线程2:>903
D: 线程3:>903
D: 线程1:>902
D: 线程2:>901
D: 线程3:>900
//使用同步时
D: 线程1:>1000
D: 线程2:>999
D: 线程2:>998
D: 线程1:>997
D: 线程2:>996
D: 线程1:>995
D: 线程2:>994
D: 线程1:>993
D: 线程2:>992
D: 线程1:>991
D: 线程2:>990
D: 线程1:>989
D: 线程2:>988
D: 线程1:>987
D: 线程2:>986
D: 线程1:>985
D: 线程2:>984
D: 线程1:>983
3.1 锁机制
为了解决异步的问题,JAVA提供了锁的机制,synchronized 关键字.可以很方便的实现线程的同步.
先理解如何进行手动加锁,这样更容易理解自动加锁的机制.
3.1.1 认识重入锁与条件对象
- 重入锁:对资源进行手动枷锁.
- ReentrantLock() : 创建一个ReentrantLock实例
- lock() : 获得锁
- unlock() : 释放锁
- 条件对象: 使线程满足某一条件后才能执行,不满足则进阻,放锁.用于管理已获得锁但是暂时没作用的线程.
- lock.newCondition : 获得一个条件对象.
- condition.await() :进阻,放锁.相当于obj.wait方法.
- condition.signal/signalAll : 唤醒在此条件上阻塞的任意某个线程/所有线程.相当于obj.notify/notifyAll
伪代码
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition;
lock.lock();
try {
if(count == 0){
//进阻,放锁
condition.await();
}
//唤醒因此条件,而阻塞的所有线程
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
}finally{
lock.unlock();
}
3.1.2 synchronized 关键字
资源互斥,线程数据同步,提供了自动加锁.同步本质见java内存模型.
- 对象锁: 每一个对象有一个内部锁,并且只有一个内部条件.
- 对应synchronized关键字,如果是多个线程访问同个对象的sychronized块,才是同步的,但是访问不同对象的话就是不同步的。
- 类锁: 是一个全局锁
- 对应static sychronized关键字,无论是多线程访问单个对象还是多个对象的sychronized块,都是同步的
在实际开发中大多数情况使用同步方法与同步代码块,实现同步,除非一些需要高度控制锁的则使用重入锁和条件对象.
3.2 实现同步的方式
线程同步的四种方式.
1. 同步方法
private synchronized void countSyncMethod() {
if (count > 0) {
Log.d(TAG, Thread.currentThread().getName() + ":>" + count--);
} else {
isRunning = false;
}
}
2. 同步代码块
private void countSyncCode() {
synchronized (this) {
if (count > 0) {
Log.d(TAG, Thread.currentThread().getName() + ":>" + count--);
} else {
isRunning = false;
}
}
}
3. 使用重入锁
private void countSyncLock() {
mLock.lock();
try {
if (count > 0) {
Log.d(TAG, Thread.currentThread().getName() + ":>" + count--);
} else {
isRunning = false;
}
} catch (Exception e) {
e.printStackTrace();
}inally{
mLock.unlock();
}
}
4. 使用特殊域变量(volatile)
3.3 volatile 关键字
先了解java内存模型与“三性”知识.
3.3.1 Java内存模型
java内存模型定义了线程和主内存之间的抽象关系.
- 所有线程的共享变量在主内存中.
- 每个线程都有一个本地内存.
线程在对变量进行存与取时,一般先改变工作内存的变量值,再在某个时机刷新到主存中去.这样就会导致多线程并发时另一个线程从主存中获取到的不一定是最新的值.
3.3.2 Java并发编程的原子性、可见性、有序性
- 原子性:对于基本数据类型只是简单的读取和赋值(将数字赋值给某个变量),仅有一个操作就是原子性操作,操作是不可中断的.
- 可见性:一个线程的修改对另外一个线程是立即可见的.
- 即volatile修饰的变量,如果在一个线程修改值,则会立即更新到主存中去,那么另一个线程会获取到最新的值.
- 有序性:编译时和运行时会对指令进行重新排序,会影响多线程并发执行的正确性.
这样当一个共享变量被volatile修饰时.
- 保证可见性,即一个线程对变量值进行了修改,另一个线程可以立即获取到最新修改后的值.
- 保证有序性,即禁止指令重排序.
- 不保证原子性.
- 所以不适用于修饰如自增自减等一些依赖于自身或者其他变量值的变量时.
private int x;
private int y;
//依赖自身
x++;
//依赖其他变量
if(x > y){
x = y;
}
四. 线程的中断
- 关键字: interrupted
中断目标线程,并不是指立即停止线程,而仅仅只是将线程的标识为true,一般由目标线程自己去检测并决定是否终止线程。
示例代码
@Override
public void run() {
//中断目标线程,并不是指立即停止线程,而仅仅只是将线程的标识为true,一般由目标线程自己去检测并决定是否终止线程.
for (int j = 0; j <100000000 ; j++) {
if (Thread.currentThread().isInterrupted()){
System.out.println("Interrupted!已经中断停止输出.开始收尾工作1");
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
//处于阻塞状态的线程,也会立马被中断,所以就会抛出此异常.
System.out.println("Interrupted!已经中断停止输出.开始收尾工作2");
return;
}
System.out.println("还没中断继续输出j:" + j);
}
}
关于Thread基础相关就介绍到这里了.接着下一篇介绍多线程编程中的线程池.Demo源码在最后一篇一起给出.
由于本人技术有限,如有错误的地方,麻烦大家给我提出来,本人不胜感激,大家一起学习进步.
参考链接: