Android中的线程使用与Java有何不同?
目录
Java中的线程
- Java中如何创建线程
- Java中的线程同步问题(synchronized关键字,lock, wait,notify,notifyall)
- Java中保证成员变量访问的同步和原子操作
- Java中如何终止线程
Android中的线程
- 线程间的通信Handler,Looper,MessageQueue
- ThreadLocal
- Asynctask引起内存泄漏
下面开始正文
Java中的线程
提起android中去使用线程,我们首先必须搞懂java中线程的一些基本概念.
1, Java中如何创建线程
在java中创建线程的几种方式
第一种: 直接new Thread
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.print("执行线程");
}
});
thread.start();
第二种: 线程工厂
ThreadFactory factory = new ThreadFactory() {
@Override
public Thread newThread(@NonNull Runnable r) {
return new Thread(r, "线程名字");
}
};
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " started!");
}
};
Thread thread1 = factory.newThread(runnable);
thread1.start();
第三种: 线程池
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread with Runnable started!");
}
};
Executor executor = Executors.newCachedThreadPool();
executor.execute(runnable);
2,Java中的线程同步问题
在使用线程过程,难免会遇到线程同步的问题,首先必须清楚线程不同步是如何产生的,然后再来看看解决方法.
下面看一段示例代码,它会出现一个情况就是线程不同步.
public static class ThreadTest{
private static int x,y;
public static void startThread1(){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100_00_00; i++){
x = i;
y = i;
}
if (x != y){
System.out.println("x != y x = "+x+" y = "+y);
}
}
});
thread.start();
}
public static void startThread2(){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100_00_00; i++){
x = i;
y = i;
}
if (x != y){
System.out.println("x != y x = "+x+" y = "+y);
}
}
});
thread.start();
}
}
我们运行startThread1()和startThread2()会输出
x != y x = 394076 y = 396613
这是因为出现了线程的不同步才产生的异常现象, 这是在线程1运行过程中,线程2也在运行导致多线程操作x,y,从而x y 不相等.
为了避免线程的不同步,java中引入了synchronized关键字. 下面对需要线程同步的代码块加入synchronized关键字.
public static class ThreadTest{
private static int x,y;
public static void startThread1(){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (ThreadDemoActivity.class){
for (int i = 0; i < 100_00_00; i++){
x = i;
y = i;
}
if (x != y){
System.out.println("x != y x = "+x+" y = "+y);
}
}
}
});
thread.start();
}
public static void startThread2(){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (ThreadDemoActivity.class){
for (int i = 0; i < 100_00_00; i++){
x = i;
y = i;
}
if (x != y){
System.out.println("x != y x = "+x+" y = "+y);
}
}
}
});
thread.start();
}
}
这下就不会有前面提到的线程不同步的问题了. synchronized关键字有几种不同的使用方法
synchronized关键字加到方法前面
public synchronized void demo(){
//...
}
synchronized定义代码块
private Object lock = new Object();
public void demo(){
synchronized (lock){
//...
}
}
synchronized都会去关联到一个锁对象,前者加到方法前面的synchronized,它的锁对象是类的对象,后者则是使用自定义的锁对象,前者更加方便,但是它只能指定一个类对象作为锁对象,后者更加灵活自由,可以定义同步代码块,自定义锁对象(就像刚开始的示例代码中使用了ThreadDemoActivity.class作为锁对象,当然也可以自定义一个对象作为锁对象)
synchronized除了解决上面提到的线程互斥访问问题,还会会解决另外一个问题数据的同步问题. 当我们在代码中有一个成员变量
x = 1
一个线程a要对x = 5赋值,它需要分三个步骤,一个是拷贝x成员变量到它线程所属的内存区域,二是把x的值赋成5,三是把x = 5放回原有的内存区域. 因为这样做会提高效率。synchronized在解决线程间同步问题的时候也顺带解决了这个数据的同步问题。
所以总结一下:
synchronized解决了两个问题,问题一数据访问的互斥,问题二是数据的同步问题
为了解决线程间的同步问题,除了使用synchronized关键字之外,还可以使用lock来解决,不过它用起来会比较麻烦。通常的代码格式是:
private Lock lock = new ReentrantLock();
public void demo(){
lock.lock();
//同步代码块
lock.unlock();
}
用得比较常见的地方的读写锁,在一个线程写的时候不允许别的线程写和读,但是在一个线程读的情况下,运行别的线程读,但是不能写。
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private int x = 1;
public void write(){
writeLock.lock();
x = 5;
writeLock.unlock();
}
public void print(){
readLock.lock();
System.out.print("x = "+x);
readLock.unlock();
}
关于wait, notify和notifyAll的使用.
在程序设计中,我们会有一个线程必须满足一个条件才能继续执行的需求,而这个条件是另外一个线程去达成的,可以看下下面代码
private String shareMsg = null;
private synchronized void initshareMsg(){
shareMsg = "share msg";
}
private synchronized void printshareMsg(){
while (shareMsg != null){
System.out.print(shareMsg);
}
}
public void run(){
Thread thread_1 = new Thread(() -> {
initshareMsg();
});
Thread thread_2 = new Thread(() -> {
printshareMsg();
});
thread_2.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread_1.start();
}
很显然上面的代码会出现死锁的现象,printshareMsg会拿到锁一直循环下去,initshareMsg永远拿不到monitor,所以不能执行, 我们可以通过wait和notify的配合使用来达到我们想要的效果. 修改后代码如下.
private String shareMsg = null;
private synchronized void initshareMsg(){
shareMsg = "share msg";
notify(); //放弃monitor,通知之前wait的线程,继续执行
}
private synchronized void printshareMsg(){
while (shareMsg == null){
try {
wait(); //等待,放弃monitor,让别的线程获得monitor
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(shareMsg);
}
}
public void run(){
Thread thread_1 = new Thread(() -> {
initshareMsg();
});
Thread thread_2 = new Thread(() -> {
printshareMsg();
});
thread_2.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread_1.start();
}
那么notifyall又有什么用?如果有多个线程都处于wait状态,那么仅仅调用notify只能唤醒一个线程,而notifyall可以唤醒所有处于wait的线程来竞争monitor.
3, Java中保证成员变量访问的同步和原子操作
关键字:volatile, Atomic相关类
上面提到了,synchronized解决了数据访问的互斥和数据的同步问题,但是单独对一个成员变量来说不能加synchronized关键字,而我们仅仅对一个成员变量做数据访问的互斥和数据的同步加上synchronized关键字又会感觉太麻烦,目前就有一个很好解决这个问题的方法是使用
volatile关键字和Atomic相关类,它们的作用容易混淆.
volatile主要用来解决的是数据的同步问题
如上图,未加volatile的情况下线程b获取到x的值可能出现x = 1的情况,加上volatile关键字就可以避免这个问题.
Atomic相关类主要解决的问题是像a++这样的操作,a++这个操作其实分成了两步,一步是r = a+1, 第二步是 a = r. 这显然不是一个原子操作,使用AtomicInteger则把a++封装成为一个原子操作.
AtomicInteger a = new AtomicInteger();
a.incrementAndGet();
a++不能靠volatile保证原子操作,volatile解决的是同步问题Atomic相关的类解决的原子操作问题.
4, Java中如何终止线程
通常我们开启一个线程的后,会有终止一个线程的需求,那么在java中是如何去终止线程呢?可以通过调用
thread.stop()
它可以立即终止线程,但是它有个不好的地方是在于,立即终止是存在一定的风险,因为线程正常执行过程中立即终止会导致程序出现异常。所以stop这个方法是被弃用的,而正规终止线程的方式是使用interrupt去终止线程.
Thread thread = new Thread(){
@Override
public void run() {
super.run();
for (int i = 0; i < 100; i++){
if (isInterrupted()){
//线程终止,收尾操作
}
}
}
};
thread.interrupt();
通过isInterrupted判断外界是否调用了线程终止,从而进行一些收尾操作后终止线程,这样显然更加安全. 另外Thread还有一个判断终止的接口是Thread.interrupt(),它和isInterrupted()的区别是Thread.interrupt()调用后会把中断标志位重置,意思就是Thread.interrupt()第一次调用后是true,第二次调用后就是false了.
另外我们在使用Thread.sleep通常会抛出InterruptedException
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
这个InterruptedException,就是在线程休眠过程中调用interrupt就会抛出该异常.
Android中的线程
1,线程间的通信Handler,Looper,MessageQueue
Looper
其实Android中线程和java线程区别就是,Android提供了一种创建无限循环线程的模式, 就是looper机制,我们来看看如何在java中去创建无限循环的线程.
private void run(){ //执行入口
CustomizableThread customizableThread = new CustomizableThread();
//设置任务
customizableThread.setTask(new Runnable() {
@Override
public void run() {
System.out.print("执行任务");
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//退出循环
customizableThread.quit();
}
/**
* 创建一个无限循环的线程
*/
class CustomizableThread extends Thread {
private Runnable task;
private boolean quit;
synchronized void setTask(Runnable task){
this.task = task;
}
synchronized void quit(){
quit = true;
}
@Override
public void run() {
super.run();
while (!quit){
synchronized (this){
if (task != null){
task.run();
task = null;
}
}
}
}
}
上面这种无限循环的线程就是Android经常提到的Looper的原型了. 在Android中它把上面提到的无限循环的机制写成了一个 Looper对象,大致如下(当然实际代码肯定比这个复杂很多,这里只是为了说明Looper到底起到了什么作用)
class Looper {
private Runnable task;
private boolean quit;
synchronized void setTask(Runnable task){
this.task = task;
}
synchronized void quit(){
quit = true;
}
public void loop(){
while (!quit){
synchronized (this){
if (task != null){
task.run();
task = null;
}
}
}
}
}
另外looper会把这个task做成一个队列的形式,那就是MessageQueue了,那么Handler又是起到什么作用呢,Handler它其实一个关联一个looper的对象,用于像looper中MessageQueue发送消息,大致的模型如下:
ThreadLocal
接下来说下ThreadLocal,ThreadLocal是用来存放线程独立的对象,什么是线程独立的线程对象,我们知道线程之间是可以共享内存的,
public class ThreadLocalDemo {
private Integer mInteger = new Integer(1); //线程之间是共享的.
private void run(){
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
mInteger = 2;
}
});
thread1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.print(mInteger);
}
});
thread2.start();
}
}
那么如果使用ThreadLocal的话就可以做到内存的独立。
public class ThreadLocalDemo {
private Integer mInteger = new Integer(1);
static ThreadLocal<Integer> sThreadLocal1 = new ThreadLocal<>();
static ThreadLocal<Integer> sThreadLocal2 = new ThreadLocal<>();
public void run(){
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
sThreadLocal1.set(mInteger);
Integer integer = sThreadLocal1.get();
integer = 3;
System.out.println("ThreadLocal thread1 integer "+integer);
}
});
thread1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
sThreadLocal2.set(mInteger);
Integer integer = sThreadLocal2.get();
System.out.println("ThreadLocal thread2 integer "+integer);
}
});
thread2.start();
}
}
I/System.out: ThreadLocal thread1 integer 3
I/System.out: ThreadLocal thread2 integer 1
上面结果输出可以说明thread1对integer的赋值只对thread1生效.
实际上在android中looper就是用ThreadLocal进行存放的,这样可以做到每个线程之间looper是独立的.
public final class Looper {
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
//其余代码省略...
}
Asynctask引起内存泄漏
什么是内存泄漏,内存泄漏是GC Root引用链中有无用的对象。这里的GC Root有3种
1,正在运行的线程
2,static变量
3,native引用到的
Asynctask实际上是后台启动线程,返回到ui线程的一个过程,它常常会引用到外部的Activity引用,导致Activity的内存泄漏,但是这是暂时的,通常情况下Asynctask不会长期引起内存泄漏.
最后
在现在这个金三银四的面试季,我自己在网上也搜集了很多资料做成了文档和架构视频资料免费分享给大家【包括高级UI、性能优化、架构师课程、NDK、Kotlin、混合式开发(ReactNative+Weex)、Flutter等架构技术资料】,希望能帮助到您面试前的复习且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。