Java中的线程基础知识
1、线程概念
线程是程序运行的基本执行单元。当操作系统(不包括单线程的操作系统,如微软早期的DOS)在执行一个程序时,会在系统中建立一个进程,而在这个进程中,必须至少建立一个线程(这个线程被称为主线程)来作为这个程序运行的入口点。因此,在操作系统中运行的任何程序都至少有一个主线程
线程的Java抽象内存模型如下,由此可见
1)每个线程都有自己独立的工作内存
2)线程1无法访问线程2的工作内存
3)线程在访问共享数据时,会把主内存中的共享变量复制到自己的工作内存中,线程操作的是工作内存中数据的副本
2、Java中线程实现的方式
方式一:实现 Runnable 接口
class MyThread implements Runnable{ // 实现Runnable接口,作为线程的实现类
private String name ; // 表示线程的名称
public MyThread(String name){
this.name = name ; // 通过构造方法配置name属性
}
public void run(){ // 覆写run()方法,作为线程 的操作主体
for(int i=0;i<10;i++){
System.out.println(name + "运行,i = " + i) ;
}
}
};
public class RunnableDemo01{
public static void main(String args[]){
MyThread mt1 = new MyThread("线程A ") ; // 实例化对象
MyThread mt2 = new MyThread("线程B ") ; // 实例化对象
Thread t1 = new Thread(mt1) ; // 实例化Thread类对象
Thread t2 = new Thread(mt2) ; // 实例化Thread类对象
t1.start() ; // 启动多线程
t2.start() ; // 启动多线程
}
};
方式二:继承 Thread 类
class MyThread extends Thread{ // 继承Thread类,作为线程的实现类
private String name ; // 表示线程的名称
public MyThread(String name){
this.name = name ; // 通过构造方法配置name属性
}
public void run(){ // 覆写run()方法,作为线程 的操作主体
for(int i=0;i<10;i++){
System.out.println(name + "运行,i = " + i) ;
}
}
};
public class ThreadDemo02{
public static void main(String args[]){
MyThread mt1 = new MyThread("线程A ") ; // 实例化对象
MyThread mt2 = new MyThread("线程B ") ; // 实例化对象
mt1.start() ; // 调用线程主体
mt2.start() ; // 调用线程主体
}
};
两者的区别和联系:
1)Thread类也是Runnable接口的子类
2)Thread是类,而Runnable是接口。类和接口区别,类只能继承一次,而接口可以实现多个
3)最重要的分享资源功能,一般我们使用多线程就是快速解决资源问题。Runnable可以实现资源分享,类实现Runnable并不具备线程功能,必须通过new Thread(runabble子类)调用start()启动线程,所以我们通常new一个runnable的子类,启动多个线程解决资源问题。Thread是类所以我们每次new一个对象时候资源已经实例化了,不能资源共享,Thread类要实现资源共享,可以声明变量为static,类共享的可以解决。
3、线程状态及切换
Java中线程的状态可分为以下6种,也有归纳为5种的,但逻辑是类似的
1)初始(NEW):新创建了一个线程对象,但还没有调用start()方法
2)运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)
3)阻塞(BLOCKED):表示线程阻塞于锁
4)等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)
5)超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回
6)终止(TERMINATED):表示该线程已经执行完毕
改变线程状态的几个方法如下,
1)Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式
2)Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间
3)thread.join()/thread.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的
4)obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒
5)obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程
6)LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines), 当前线程进入WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒
4、线程优先级
最低优先级 1:Thread.MIN_PRIORITY
最高优先级 10:Thread.MAX_PRIORITY
普通优先级 5:Thread.NORM_PRIORITY
使用setPriority(Thread.MIN_PRIORITY)方法设置线程优先级。一般优先级较高的线程先执行run()方法,但是这个不是确定的。因为Java使用的是抢占式调度模型,线程的优先级具有 “随机性”,所以设置了高优先级的线程会占用更多资源,总体上会优先执行
5、线程同步
1)同步方法
用synchronized关键字修饰方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态
2)同步代码块
用synchronized关键字修饰语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可
3)Volatile
a.volatile关键字为域变量的访问提供了一种免锁机制
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
4)使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力
5)ThreadLocal方式
ThreadLocal类的常用方法有如下几个:
ThreadLocal(),创建一个线程本地变量
get(),返回此线程局部变量的当前线程副本中的值
initialValue(),返回此线程局部变量的当前线程的"初始值"
set(T value),将此线程局部变量的当前线程副本中的值设置为value
ThreadLocal.ThreadLocalMap.Entry中的key是弱引用的,但是value是基于强引用的,当某个ThreadLocal对象不存在强引用时,且GC后,key会被回收,但是value还存强引用时,因此就会出现内存的泄露情况。目前最好的解决方式就是在使用完ThreadLocal及时调remove方法
6、线程安全
线程安全就是多线程访问时采用了加锁机制,当一个线程访问该类的某个数据时进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
Java中典型的非线程安全和线程安全的集合类对比如下列表。在需要考虑线程安全问题的情况下,使用线程安全的集合类实现较简单,但会影响性能,所以通常大多数的集合类是非线程安全的
非线程安全 | 线程安全 |
---|---|
ArrayList | Vector |
HashMap | HashTable |
StringBuilder | StringBuffer |
… | … |
7、线程池
主要作用
1)降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗
2)提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行
3)提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以进行统一分配、调优和监控
线程池的创建
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
使用构造方法ThreadPoolExecutor(),各个参数的含义概述如下
1)corePoolSize: 线程池核心线程数最大值
2)maximumPoolSize: 线程池最大线程数大小
3)keepAliveTime: 线程池中非核心线程空闲的存活时间大小
4)unit: 线程空闲存活时间单位
5)workQueue: 存放任务的阻塞队列
6)threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题
7)handler: 线城池的饱和策略事件,主要有四种类型
调用execute方法的执行流程如下,
1)提交一个任务,线程池里存活的核心线程数小于线程数corePoolSize时,线程池会创建一个核心线程去处理提交的任务。
2)如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
3)当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。
4)如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理。
几种常用的线程池
1)newFixedThreadPool (固定数目线程的线程池)
适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务
2)newCachedThreadPool(可缓存线程的线程池)
适用于并发执行大量短期的小任务
3)newSingleThreadExecutor(单线程的线程池)
适用于串行执行任务的场景,一个任务一个任务地执行
4)newScheduledThreadPool(定时及周期执行的线程池)
适用于周期性执行任务的场景,需要限制线程数量的场景
8、查看线程运行情况的命令
- 查看指定进程pid,以京东为例
执行 adb shell “ps | grep jingdong”,结果如下
u0_a415 3050 554 2707208 339720 0 0 S com.jingdong.app.mall
u0_a415 3739 554 2274060 159168 0 0 S com.jingdong.app.mall:manto0
u0_a415 4167 554 1895632 82336 0 0 S com.jingdong.app.mall:jdpush
由此可知,京东app启动了三个进程,其中主进程的pid是3050 - 查看京东app主进程的线程运行情况
执行 adb shell “ps -T -p 3050”,结果如下
USER PID TID PPID VSZ RSS WCHAN ADDR S CMD
u0_a415 3050 3050 554 2704168 339868 0 0 S ngdong.app.mall
u0_a415 3050 3066 554 2704168 339868 0 0 S Jit thread pool
u0_a415 3050 3067 554 2704168 339868 0 0 S Signal Catcher
u0_a415 3050 3068 554 2704168 339868 0 0 S ADB-JDWP Connec
u0_a415 3050 3069 554 2704168 339868 0 0 S ReferenceQueueD
u0_a415 3050 3070 554 2704168 339868 0 0 S FinalizerDaemon
…
Android中的线程
1、关于应用主线程
启动应用时,系统会为该应用创建一个称为“main”(主线程)的执行线程。此线程非常重要,因为其负责将事件分派给相应的界面微件,其中包括绘图事件。此外,应用与 Android 界面工具包组件(来自 android.widget 和 android.view 软件包的组件)也几乎都在该线程中进行交互。因此,主线程有时也称为界面线程
系统不会为每个组件实例创建单独的线程。在同一进程中运行的所有组件均在界面线程中进行实例化,并且对每个组件的系统调用均由该线程进行分派。因此,响应系统回调的方法(例如,报告用户操作的 onKeyDown() 或生命周期回调方法)始终在进程的界面线程中运行
在写android界面相关代码时,需切记如下两个要点
1)不要阻塞UI线程
2)不要在UI线程之外访问Android UI工具包
2、工作线程
要保证应用界面的响应能力,关键是不能阻塞界面线程。如果执行的操作不能即时完成,则应确保它们在单独的线程(“后台”或“工作”线程)中运行
为解决此问题,Android 提供了几种途径,以便您从其他线程访问界面线程
1)Activity.runOnUiThread(Runnable)
2)View.post(Runnable)
3)View.postDelayed(Runnable, long)
4)如要通过工作线程处理更复杂的交互,可以考虑使用AsyncTask、Handler&Thread、HandlerThread、IntentService等机制实现
5) AsyncTask是一种轻量级的异步任务类,它可以在线程池中执行后台任务,然后把执行的进度和最终结果传递给主线程并在主线程中更新UI。从实现上来说,AsyncTask封装了Thread和Handler,通过AsyncTask我们更加方便的执行后台任务以及在主线程中访问UI,但是AsyncTask并不适合进行特别耗时的后台任务,对于特别耗时的任务来说,建议使用线程池
6) HandlerThread继承了Thread,它是一种可以使用Handler的Thread。它的实现也很简单,就是在run方法中通过Looper.prepare()来创建消息队列,并通过Looper.loop()来开启消息循环,这样就允许在HandlerThread中创建Handler了
7) IntentService适合于执行由UI触发的后台Service任务,并可以把后台任务执行的情况通过一定的机制反馈给UI
极速版App中的线程
1、主工程中使用的线程
自定义的工作线程类NonUIThread,主要职责是按序初始化定位、加载多dex、注册推动服务等
1)在App->MyApplication类的attach阶段创建并启动的
2)通过继承Thread类的方式实现
3)通过synchronized关键字同步方法来实现单例线程类
4)默认的线程优先级是NORM_PRIORITY
5)使用obj.wait()和notify()的方式改变线程状态
6)使用setPriority(Thread.MIN_PRIORITY)方法设置线程优先级
public class MyApplication extends Application {
private ProcessInitLifeCycle processInit;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
NonUIThread.getInstance().start();
} catch (IllegalThreadStateException e) {
// throws IllegalThreadStateException - if this thread has already started.
// I still cannot figure out the root cause yet, just catch it
e.printStackTrace();
}
loadMultiDex(base);
//所有的需要启动初始化需要做的事情在这里执行
initAppLike(base);
}
}
public class NonUIThread extends Thread{
private static final String TAG = "NonUIThread";
private Handler mHandler;
private final Object oLock = new Object();
private final Object oIdleLock = new Object();
private int previousPriority = Thread.NORM_PRIORITY;
private final AtomicBoolean started = new AtomicBoolean(false);
private NonUIThread(){
}
private static volatile NonUIThread instance;
public static synchronized NonUIThread getInstance(){
if(null == instance){
instance = new NonUIThread();
}
return instance;
}
@Override
public void run() {
if (started.getAndSet(true)) return;
Looper.prepare();
synchronized (oLock) {
mHandler = new Handler();
oLock.notify();
}
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
synchronized (oIdleLock){
oIdleLock.notify();
}
return true;
}
});
Looper.loop();
}
public void postRunnable(Runnable runnable){
synchronized (oLock) {
if(null == mHandler){
try {
oLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mHandler.post(runnable);
}
}
public void waitForIdle(){
synchronized (oIdleLock){
try {
oIdleLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
广告弹窗业务核心类ADActvity,主要职责是负责广告弹窗的处理逻辑
1)使用AsyncTask来实现线程交互
2)调用execute()方法实际上是串行执行任务。使用executeOnExecutor(Executor)可以并行执行,但是这个api有版本限制
3)doInBackground(Params…)后台执行,比较耗时的操作都可以放在这里,注意这里不能直接操作UI
4)当前页面退出时,一般需要调用cancel()方法取消当前task,否则存在异常风险
5)AsyncTask适用于后台操作只有几秒的短时操作,否则存在内存泄露风险
public class ADActivity extends BaseActivity implements View.OnClickListener {
private String homePage;
private static final String AD_SHOW_TIME = "ad_show_time";
private ADEntry adEntry;
private int timerCount = 5;
private TextView ignoreButton;
private JumpEntity jumpEntity;
private boolean isJumped;
private Handler showTimeHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
...
}
};
private long startTime;
private AsyncTask<String, Integer, Bitmap> asyncTask = new AdDisplayTask();
@Override
protected void onCreate(Bundle savedInstanceState) {
startTime = SystemClock.elapsedRealtime();
statusBarTransparentEnable = true;
super.onCreate(savedInstanceState);
...
setContentView(R.layout.activity_a_d);
...
asyncTask.execute(adPath);
}
@Override
protected void onResume() {
super.onResume();
isJumped = false;
if (showTimeHandler != null) {
showTimeHandler.sendEmptyMessageDelayed(0, 1000);
} else {
onIgnore(null);
}
}
@Override
protected void onStop() {
super.onStop();
finish();
}
@Override
public void finish() {
if (showTimeHandler != null) {
if (showTimeHandler.hasMessages(0))
showTimeHandler.removeMessages(0);
showTimeHandler = null;
}
if (asyncTask != null && !asyncTask.isCancelled()) {
asyncTask.cancel(false);
}
super.finish();
}
@Override
protected void onDestroy() {
...
super.onDestroy();
}
@Override
public void onClick(View v) {
onJump(v);
}
class AdDisplayTask extends AsyncTask<String, Integer, Bitmap> {
@Override
protected Bitmap doInBackground(String... strings) {
try {
if (strings == null || strings.length == 0) return null;
String path = strings[0];
if (TextUtils.isEmpty(path) || !new File(path).exists()) return null;
return BitmapFactory.decodeFile(path);
} catch (Throwable e) {
return null;
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
...
}
}
}
日志上报核心类LogReporter,主要职责是上报各页面的简单日志信息
1)使用了newFixedThreadPool线程池,长度设定为5
2)默认的线程优先级是MIN_PRIORITY,不抢占其他线程的资源
3)创建ThreadFactory创建新线程,维护线程工厂
4)有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象
public class LogReporter extends AbsLogReporter {
...
private ExecutorService es;
LogReporter(String strategyParam) {
es = Executors.newFixedThreadPool(5, new ThreadFactory(){
@Override
public Thread newThread(@NonNull Runnable r) {
Thread thread = new Thread(r);
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
}
});
if (!TextUtils.isEmpty(strategyParam)) {
param = JDJSON.parseObject(strategyParam, LogStrategyParam.class);
if (null != param) {
param.parseParams();
JDActivityLifeCycleCallBack callBack = JDActivityLifeCycleCallBack.getInstance();
callBack.setParam(param);
JdSdk.getInstance().getApplication().registerActivityLifecycleCallbacks(callBack);
}
}
process = ProcessUtil.getProcessName(JdSdk.getInstance().getApplication());
}
...
@Override
public void report(final HashMap<String, String> data) {
es.execute(new Runnable() {
@Override
public void run() {
ArrayList<HashMap<String,String>> list = new ArrayList<>();
list.add(data);
boolean r = PerformanceReporter.reportData(list);
}
});
}
/**
* report very simple msg for INNER, do not open for other package
*
* @param msg
*/
void reportSimpleMessage(String msg) {
HashMap<String, String> map = getAdditionalData();
map.put("exceptionType", "");
map.put("className", "");
map.put("msg", msg);
map.put("methodStack", "");
map.put("occurTime", String.format("%.6f", System.currentTimeMillis() / 1000.0f));
map.put("logLevel", "INNER");//private log level
map.put("logTag", "ALC");//ActivityLifeCycle
report(map);
}
}
2、三方框架网络库中使用的线程
3、业务模块首页中使用的线程
1)在HomeUtil中封装了切换线程的处理逻辑
2)首页模块的各页面更新UI统一通过runOnUiThread()方法分发
3)首页模块只有PageInfoUtils类的collectCrashData()方法中直接new Thread(),但这个函数没有被调用
public class HomeUtil {
public static Handler sHandler = new Handler(Looper.getMainLooper());
...
/**
* @return 是否主线程
*/
public static boolean isMainThread() {
return Looper.myLooper() == Looper.getMainLooper();
}
/**
* @return 是否子线程
*/
public static boolean isSubThread() {
return Looper.myLooper() != Looper.getMainLooper();
}
/**
* 判断是否主线程,主线程直接执行,否则post至主线程。
*/
public static void runOnUiThread(BaseRun runnable) {
if (isMainThread()) {
runnable.run();
} else {
sHandler.post(runnable);
}
}
/**
* 不判断是否主线程,直接post至主线程。
*/
public static void postOnUiThread(BaseRun runnable) {
sHandler.post(runnable);
}
public static void runOnUiThread(BaseRun runnable, long delayMillis) {
sHandler.postDelayed(runnable, delayMillis);
}
...
}
4、业务模块任务中心使用的线程
1)TaskFloatActivity类中的读取通讯录方法readContactList()中,通过new Thread()开启了线程
2)在线程函数中弱引用了当前的TaskFloatActivity类对象,当系统执行gc的时候该对象会被回收,读取联系的人方法buildContactsDataWithRegion()是否会抛异常待验证???
3)如果当前页面TaskFloatActivity退出了,这里通过isDestroy变量控制线程的退出,以保证后面的操作不再执行
4)建议在通讯录sdk里做修改,当TaskFloatActivity退出后,通过全局变量的方式让读取联系人的方法buildContactsDataWithRegion()快速执行完
public class TaskFloatActivity extends MvpBaseActivity<TaskCenterPresenter, BaseNavigator> implements IMissionView, PullToRefreshBase.OnRefreshListener<RecyclerView>, View.OnClickListener, MissionActiveItemView.ItemClickCallback, INewUserInterface {
...
/**
* 获取通讯录明细
*/
private void readContactList() {
if (isDestroy || isReading) {
return;
}
new Thread(new Runnable() {
@Override
public void run() {
try {
isReading = true;
//获取通讯录list
WeakReference<Activity> contextWeakReference = new WeakReference<Activity>(getThisActivity());
JSONArray phoneList = ContactUtils.getPhoneList(contextWeakReference.get());
if (isDestroy) {
isReading = false;
return;
}
if (null != phoneList && phoneList.length() > 0) {
if (Build.VERSION.SDK_INT < 23) {
//小于6.0的设备,读到证明有权限
updateContactList(3, phoneList);
} else {
//上传
ContactUtils.upload(MissionUtils.CONTACT_ACT_ID, phoneList);
}
} else {
//通讯录为空,小于6.0的设备
if (Build.VERSION.SDK_INT < 23) {
post(new Runnable() {
@Override
public void run() {
showErrorDialog();
}
});
}
}
isReading = false;
} catch (Exception e) {
isReading = false;
e.printStackTrace();
}
}
}).start();
}
...
}
5、线程使用注意事项
1)更新UI务必在主线程,极速版使用的网络框架回调是在子线程里执行的,需要切到主线程更新UI
2)尽量减少使用new Thread()方式,使用时充分考虑线程安全问题,页面退出时要确保线程快速执行完(满足需求的前提下)
3)如果页面退出后,线程仍需要继续执行完,建议使用Service来实现
4)异步任务比较多时,应该使用线程池
5)虽然使用多线程可以提高程序的并发量,但是我们需要特别注意因为引入多线程而可能伴随而来的内存问题