Java进阶 ——— Java多线程(四)之多线程局部变量TreadLocal

在了解ThreadLocal之前,一定要确定一个概念:ThreadLocal不是用来解决共享对象的多线程访问问题的
那么ThreadLocal在多线程的作用是什么呢?从下面几个方面来了解

ThreadLocal的作用

ThreadLocal可以理解为:线程局部变量, 是每一个线程所单独持有的。其他线程不能对其进行访问, 通常是类中的 private static 字段,是对该字段初始值的一个拷贝,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联,

在并发编程的时候,成员变量如果不做任何处理其实是线程不安全的,各个线程都在操作同一个变量,显然是不行的,并且我们也知道volatile这个关键字也是不能保证线程安全的。当然我们可以使用synchorinized 关键字来为此变量加锁,进行同步处理,从而限制只能有一个线程来使用此变量,但是加锁会大大影响程序执行效率,此外我们还可以使用ThreadLocal来解决对某一个变量的访问冲突问题。

当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。

深入了解ThreadLocal使用

先不去分析源码,仅仅写个例子,根据例子学习ThreadLocal的简单使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ThreadTest {
//定义一个全局ThreadLocal
public static ThreadLocal<String> locals= new ThreadLocal<String>();
//开启三个线程
public void threadLocalTest(){
ThreadLocalTest test1 = new ThreadLocalTest("AA");
ThreadLocalTest test2 = new ThreadLocalTest("BB");
ThreadLocalTest test3 = new ThreadLocalTest("CC");
test1.start();
test2.start();
test3.start();
}
}

在线程里往ThreadLocal塞值,再打印出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ThreadLocalTest extends Thread {
private int a = 5;
public ThreadLocalTest(String name) {
super(name);
}
@Override
public void run() {
super.run();
try {
for (int i = 0; i < 5; i++) {
a += 1;
ThreadTest.locals.set(a + "");
Log.e(this.getName(), "value ===== " + ThreadTest.locals.get());
Thread.sleep(300);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

查看运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
10-24 10:31:46.340 9307-9350/com.t9.news E/AA: value ===== 6
10-24 10:31:46.340 9307-9351/com.t9.news E/BB: value ===== 6
10-24 10:31:46.343 9307-9352/com.t9.news E/CC: value ===== 6
10-24 10:31:46.640 9307-9350/com.t9.news E/AA: value ===== 7
10-24 10:31:46.641 9307-9351/com.t9.news E/BB: value ===== 7
10-24 10:31:46.644 9307-9352/com.t9.news E/CC: value ===== 7
10-24 10:31:46.940 9307-9350/com.t9.news E/AA: value ===== 8
10-24 10:31:46.941 9307-9351/com.t9.news E/BB: value ===== 8
10-24 10:31:46.944 9307-9352/com.t9.news E/CC: value ===== 8
10-24 10:31:47.240 9307-9350/com.t9.news E/AA: value ===== 9
10-24 10:31:47.241 9307-9351/com.t9.news E/BB: value ===== 9
10-24 10:31:47.244 9307-9352/com.t9.news E/CC: value ===== 9
10-24 10:31:47.540 9307-9350/com.t9.news E/AA: value ===== 10
10-24 10:31:47.541 9307-9351/com.t9.news E/BB: value ===== 10
10-24 10:31:47.545 9307-9352/com.t9.news E/CC: value ===== 10

看到每个线程的里都有自己的String,并且互不影响—-,不存在一个线程修改另一个线程中值得情况,对于同一个ThreadLocal对象而言,内部数据仅为自己独有,其他线程无法修改

ThreadLocal的理解

了解了ThreadLocal的使用,接下来肯定要看看源码,分析内部实现方式。

就从ThreadLocal 几个主要方法来学习

1
2
3
public T get()
public void set(T value)
public void remove()

get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本

首先我们来看一下ThreadLocal类是如何为每个线程创建一个变量的副本的。

先来看get方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
//先获取当前线程
Thread t = Thread.currentThread();
//然后getMap(t)方法获取到一个map,类型为ThreadLocalMap
ThreadLocalMap map = getMap(t);
//不为空
if (map != null) {
//然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是 this,而不是当前线程t。
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//如果获取成功,则返回value值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//否则调用setInitialValue方法返回value
return setInitialValue();
}

仔细看看每一步的操作:

  • getMap(t)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * Get the map associated with a ThreadLocal. Overridden in
    * InheritableThreadLocal.
    *
    * @param t the current thread
    * @return the map
    */
    ThreadLocalMap getMap(Thread t) {
    //返回当前线程中的成员变量threadLocals
    return t.threadLocals;
    }

那么继续查看,进入Thread.class ,成员变量threadLocals是什么

1
2
3
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap是ThreadLocal类的一个内部类,我们继续取看ThreadLocalMap的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键值

如果getMap为null,则返回setInitialValue()

  • setInitialValue()方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
    * Variant of set() to establish initialValue. Used instead
    * of set() in case user has overridden the set() method.
    *
    * @return the initial value
    */
    private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    //就是如果map不为空,就设置键值对,
    if (map != null)
    map.set(this, value);
    else
    //为空,再创建Map
    createMap(t, value);
    return value;
    }
  • createMap(t, value)的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    * Create the map associated with a ThreadLocal. Overridden in
    * InheritableThreadLocal.
    *
    * @param t the current thread
    * @param firstValue value for the initial entry of the map
    */
    void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

创建ThreadLocalMap对象并赋值给Thread中的threadLocals

经过这一系列流程,ThreadLocal是为每个线程创建变量的副本就很清晰:

  • 首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

  • 初始化Thread时,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

  • 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

总结

1。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
2。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦
3.通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中
4.为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量
5.在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
因为get方法中,getMap()默认为null,则返回setInitialValue(),setInitialValue()方法中, T value = initialValue() 默认返回null,最终会报空指针异常


参考

http://www.cnblogs.com/dolphin0520/p/3920407.html

Java进阶 ——— Java多线程(4)之多线程局部变量TreadLocal

在了解ThreadLocal之前,一定要确定一个概念:ThreadLocal不是用来解决共享对象的多线程访问问题的
那么ThreadLocal在多线程的作用是什么呢?从下面几个方面来了解

ThreadLocal的作用

ThreadLocal可以理解为:线程局部变量, 是每一个线程所单独持有的。其他线程不能对其进行访问, 通常是类中的 private static 字段,是对该字段初始值的一个拷贝,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联,

在并发编程的时候,成员变量如果不做任何处理其实是线程不安全的,各个线程都在操作同一个变量,显然是不行的,并且我们也知道volatile这个关键字也是不能保证线程安全的。当然我们可以使用synchorinized 关键字来为此变量加锁,进行同步处理,从而限制只能有一个线程来使用此变量,但是加锁会大大影响程序执行效率,此外我们还可以使用ThreadLocal来解决对某一个变量的访问冲突问题。

当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。

深入了解ThreadLocal使用

先不去分析源码,仅仅写个例子,根据例子学习ThreadLocal的简单使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ThreadTest {
//定义一个全局ThreadLocal
public static ThreadLocal<String> locals= new ThreadLocal<String>();
//开启三个线程
public void threadLocalTest(){
ThreadLocalTest test1 = new ThreadLocalTest("AA");
ThreadLocalTest test2 = new ThreadLocalTest("BB");
ThreadLocalTest test3 = new ThreadLocalTest("CC");
test1.start();
test2.start();
test3.start();
}
}

在线程里往ThreadLocal塞值,再打印出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ThreadLocalTest extends Thread {
private int a = 5;
public ThreadLocalTest(String name) {
super(name);
}
@Override
public void run() {
super.run();
try {
for (int i = 0; i < 5; i++) {
a += 1;
ThreadTest.locals.set(a + "");
Log.e(this.getName(), "value ===== " + ThreadTest.locals.get());
Thread.sleep(300);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

查看运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
10-24 10:31:46.340 9307-9350/com.t9.news E/AA: value ===== 6
10-24 10:31:46.340 9307-9351/com.t9.news E/BB: value ===== 6
10-24 10:31:46.343 9307-9352/com.t9.news E/CC: value ===== 6
10-24 10:31:46.640 9307-9350/com.t9.news E/AA: value ===== 7
10-24 10:31:46.641 9307-9351/com.t9.news E/BB: value ===== 7
10-24 10:31:46.644 9307-9352/com.t9.news E/CC: value ===== 7
10-24 10:31:46.940 9307-9350/com.t9.news E/AA: value ===== 8
10-24 10:31:46.941 9307-9351/com.t9.news E/BB: value ===== 8
10-24 10:31:46.944 9307-9352/com.t9.news E/CC: value ===== 8
10-24 10:31:47.240 9307-9350/com.t9.news E/AA: value ===== 9
10-24 10:31:47.241 9307-9351/com.t9.news E/BB: value ===== 9
10-24 10:31:47.244 9307-9352/com.t9.news E/CC: value ===== 9
10-24 10:31:47.540 9307-9350/com.t9.news E/AA: value ===== 10
10-24 10:31:47.541 9307-9351/com.t9.news E/BB: value ===== 10
10-24 10:31:47.545 9307-9352/com.t9.news E/CC: value ===== 10

看到每个线程的里都有自己的String,并且互不影响—-,不存在一个线程修改另一个线程中值得情况,对于同一个ThreadLocal对象而言,内部数据仅为自己独有,其他线程无法修改

ThreadLocal的理解

了解了ThreadLocal的使用,接下来肯定要看看源码,分析内部实现方式。

就从ThreadLocal 几个主要方法来学习

1
2
3
public T get()
public void set(T value)
public void remove()

get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本

首先我们来看一下ThreadLocal类是如何为每个线程创建一个变量的副本的。

先来看get方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
//先获取当前线程
Thread t = Thread.currentThread();
//然后getMap(t)方法获取到一个map,类型为ThreadLocalMap
ThreadLocalMap map = getMap(t);
//不为空
if (map != null) {
//然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是 this,而不是当前线程t。
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//如果获取成功,则返回value值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//否则调用setInitialValue方法返回value
return setInitialValue();
}

仔细看看每一步的操作:

  • getMap(t)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * Get the map associated with a ThreadLocal. Overridden in
    * InheritableThreadLocal.
    *
    * @param t the current thread
    * @return the map
    */
    ThreadLocalMap getMap(Thread t) {
    //返回当前线程中的成员变量threadLocals
    return t.threadLocals;
    }

那么继续查看,进入Thread.class ,成员变量threadLocals是什么

1
2
3
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap是ThreadLocal类的一个内部类,我们继续取看ThreadLocalMap的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键值

如果getMap为null,则返回setInitialValue()

  • setInitialValue()方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
    * Variant of set() to establish initialValue. Used instead
    * of set() in case user has overridden the set() method.
    *
    * @return the initial value
    */
    private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    //就是如果map不为空,就设置键值对,
    if (map != null)
    map.set(this, value);
    else
    //为空,再创建Map
    createMap(t, value);
    return value;
    }
  • createMap(t, value)的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    * Create the map associated with a ThreadLocal. Overridden in
    * InheritableThreadLocal.
    *
    * @param t the current thread
    * @param firstValue value for the initial entry of the map
    */
    void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

创建ThreadLocalMap对象并赋值给Thread中的threadLocals

经过这一系列流程,ThreadLocal是为每个线程创建变量的副本就很清晰:

  • 首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

  • 初始化Thread时,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

  • 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

总结

1。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
2。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦
3.通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中
4.为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量
5.在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
因为get方法中,getMap()默认为null,则返回setInitialValue(),setInitialValue()方法中, T value = initialValue() 默认返回null,最终会报空指针异常


参考

http://www.cnblogs.com/dolphin0520/p/3920407.html

Java进阶 ——— Java多线程(三)之多线程同步问题

引言

接上一篇,Java进阶 ——— Java多线程(二)之如何开启多线程
介绍了Java多线程的开启方法,但是多线程运行的安全问题,将是本篇的重点

延伸阅读,Java多线程系列文章

Java进阶 ——— Java多线程(一)之进程和线程
Java进阶 ——— Java多线程(二)之如何开启多线程

在第一篇文章中,提到要实现多线程安全,就要实现线程同步,那么线程同步有哪些方法呢?

介绍线程同步之前,先大概了解一下多线程的原理。

线程的执行是CPU随机调度的,比如我们开启N个线程,这N个线程并不是同时执行的,而是CPU快速的在这N个线程之间切换执行,由于切换速度极快使我们感觉同时执行罢了。发生上面问题的本质就是CPU对线程执行的随机调度,比如A线程此时正在打印信息还没打印完毕此时CPU切换到B线程执行了,B线程执行完了又切换回A线程执行就会导致第一篇文章中打印错乱问题。

线程同步问题往往发生在多个线程调用同一方法或者操作同一变量,但是我们要知道其本质就是CPU对线程的随机调度,CPU无法保证一个线程执行完其逻辑才去调用另一个线程执行。

线程同步

所以解决线程同步的思路就是:保证一个线程在执行方法的时候如果没执行完那么另一个线程不能执行此方法,换句话说就是只能等待别的线程执行完毕才能执行,确保数据在任何时刻只有一个线程可以操作,保证数据完整性

为了解决线程同步问题,引入的概念

synchronized

synchronized同步有两种方式,同步代码块和同步方法

synchronized 同步方法

在方法上加上synchronized关键字,实际上锁的是this,即当前类对象,
如下列代码,例如外部要调用run方法,则需要创建ThreadRunnable对象实例,此时添加在run方法上的锁,实际是对实例对象加锁。

1
2
3
4
5
6
7
class ThreadRunnable implements Runnable {
@Override
public synchronized void run() {
age++;
System.out.println(Thread.currentThread().getName() + "----" + age);
}
}
synchronized 同步代码块

同步代码块写法:synchronized(obj){},其中obj为锁对象,此处我们传入this,同样方法的锁也为当前对象。

1
2
3
4
5
6
7
8
9
class ThreadRunnable implements Runnable {
@Override
public void run() {
synchronized (this){ //对代码块添加锁,保证线程同步
age++;
System.out.println(Thread.currentThread().getName() + "----" + age);
}
}
}

synchronized 修饰静态类或静态方法

我们知道静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象

1
2
3
4
5
6
7
private static int count = 0;
public synchronized static void staticMethod(){
for (int i = 0; i < 10; i++) {
count++;
System.out.println(count);
}
}

synchronized修饰一个类

synchronized作用于一个类T时,是给这个类T加锁,T的所有对象用的是同一把锁。

1
2
3
4
5
6
7
8
9
10
class ThreadRunnable implements Runnable {
int a = 0;
@Override
public synchronized void run() {
synchronized (ThreadRunnable.class){
a++;
System.out.println(Thread.currentThread().getName() + "----" + a);
}
}
}

synchronized总结:

  • 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
  • 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
  • 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁(接下来会将到死锁的形成和解决方式),所以尽量避免无谓的同步控制。

    Lock

    Lock与synchronized有什么区别呢?Lock是在Java1.6被引入进来的,Lock的引入让锁有了可操作性,
    可操作性:就是我们在需要的时候去手动的获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。

Lock接口的实现子类之一ReentrantLock,翻译过来就是重入锁,就是支持重新进入的锁,该锁能够支持一个线程对资源的重复加锁,也就是说在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞,同时还支持获取锁的公平性和非公平性,所谓公平性就是多个线程发起lock()请求,先发起的线程优先获取执行权,非公平性就是获取锁与是否优先发起lock()操作无关。默认情况下是不公平的锁,为什么要这样设计呢?现实生活中我们都希望公平的啊?我们想一下,现实生活中要保证公平就必须额外开销,比如地铁站保证有序公平进站就必须配备额外人员维持秩序,程序中也是一样保证公平就必须需要额外开销,这样性能就下降了,所以公平与性能是有一定矛盾的,除非公平策略对你的程序很重要,比如必须按照顺序执行线程,否则还是使用不公平锁为好。

先通过代码了解 Lock的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class ThreadTest {
private ReentrantLock lock = new ReentrantLock();
public void threadTest() {
ThreadRunnable runnable = new ThreadRunnable();
Thread thread = new Thread();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
Thread thread3 = new Thread(runnable);
Thread thread4 = new Thread(runnable);
Thread thread5 = new Thread(runnable);
Thread thread6 = new Thread(runnable);
Thread thread7 = new Thread(runnable);
thread.start();
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
thread6.start();
thread7.start();
}
class ThreadRunnable implements Runnable {
int a = 0;
@Override
public void run() {
lock.lock(); // 获取锁对象
try {
a++;
System.out.println(Thread.currentThread().getName() + "----" + a);
} finally {
//为了防止我们代码出现异常,所以我们的释放锁操作放在finally中,因为finally中的代码无论如何都是会执行的
lock.unlock(); //释放锁对象
}
}
}
}

看一下运行结果,程序执行是没有问题的

1
2
3
4
5
6
7
10-18 14:40:33.985 2847-3642/com.t9.news I/System.out: Thread-14----1
10-18 14:40:33.987 2847-3641/com.t9.news I/System.out: Thread-13----2
10-18 14:40:33.990 2847-3643/com.t9.news I/System.out: Thread-15----3
10-18 14:40:33.994 2847-3645/com.t9.news I/System.out: Thread-17----4
10-18 14:40:33.995 2847-3640/com.t9.news I/System.out: Thread-12----5
10-18 14:40:33.997 2847-3639/com.t9.news I/System.out: Thread-11----6
10-18 14:40:33.998 2847-3644/com.t9.news I/System.out: Thread-16----7

其实在Lock还有几种获取锁的方式,我们这里再说一种就是tryLock()这个方法跟Lock()是有区别的,Lock在获取锁的时候如果拿不到锁就一直处于等待状态,直到拿到锁,但是tryLock()却不是这样的,tryLock是有一个Boolean的返回值的,如果没有拿到锁直接返回false,停止等待,它不会像Lock()那样去一直等待获取锁。

修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ThreadRunnable implements Runnable {
int a = 0;
@Override
public void run() {
if (lock.tryLock()){ //获取锁对象
try {
a++;
System.out.println(Thread.currentThread().getName() + "----" + a);
} finally {
lock.unlock(); //释放锁对象
}
}
}
}

运行程序,查看结果

1
2
3
4
5
10-18 14:43:21.365 3846-3867/com.t9.news I/System.out: Thread-5----1
10-18 14:43:21.368 3846-3866/com.t9.news I/System.out: Thread-4----2
10-18 14:43:21.370 3846-3870/com.t9.news I/System.out: Thread-8----3
10-18 14:43:21.374 3846-3869/com.t9.news I/System.out: Thread-7----4
10-18 14:43:21.375 3846-3871/com.t9.news I/System.out: Thread-9----5

很明显,有三个线程没有获取到锁对象,这时候就不等待了。那么这种方法肯定不完美,想让所有线程获取对象,但是线程发现获取不到就放弃了,
其实tryLock()方法还可以设置获取的等待时长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ThreadRunnable implements Runnable {
int a = 0;
@Override
public void run() {
try {
// 如果5秒内获取不到锁对象,那就不再等待
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
a++;
System.out.println(Thread.currentThread().getName() + "----" + a);
} finally {
lock.unlock(); //释放锁对象
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

查看结果:所有线程都获取到锁对象

1
2
3
4
5
6
7
10-18 14:50:48.466 4031-4053/com.t9.news I/System.out: Thread-5----1
10-18 14:50:48.466 4031-4057/com.t9.news I/System.out: Thread-9----2
10-18 14:50:48.470 4031-4056/com.t9.news I/System.out: Thread-8----3
10-18 14:50:48.473 4031-4054/com.t9.news I/System.out: Thread-6----4
10-18 14:50:48.476 4031-4055/com.t9.news I/System.out: Thread-7----5
10-18 14:50:48.477 4031-4052/com.t9.news I/System.out: Thread-4----6
10-18 14:50:48.481 4031-4051/com.t9.news I/System.out: Thread-3----7

Lock与synchronized同步方式优缺点

  • 实现
    Lock 的锁定是通过代码实现的,而 synchronized 是在 JVM 层面上实现的(所有对象都自动含有单一的锁。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁,其计数变为0。在线程第一次给对象加锁的时候,计数变为1。每当这个相同的线程在此对象上获得锁时,计数会递增。只有首先获得锁的线程才能继续获取该对象上的多个锁。每当线程离开一个synchronized方法,计数递减,当计数为0的时候,锁被完全释放,此时别的线程就可以使用此资源)。

  • 释放
    synchronized 在锁定时如果方法块抛出异常,JVM 会自动将锁释放掉,不会因为出了异常没有释放锁造成线程死锁。但是 Lock 的话就享受不到 JVM 带来自动的功能,出现异常时必须在 finally 将锁释放掉,否则将会引起死锁。

  • 资源
    在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronized,另外可读性非常好。在资源竞争激烈情况下,Lock同步机制性能会更好一些。

    感谢


https://www.cnblogs.com/leipDao/p/8295766.html
http://www.importnew.com/21866.html

Java进阶 ——— Java多线程(二)之如何开启多线程

引言

第一篇文章讲到了Java多线程的概念和作用,本篇文章则介绍在Java中如何开启多线程。

延伸阅读,Java多线程系列文章

Java进阶 ——— Java多线程(一)之进程和线程
Java进阶 ——— Java多线程(二)之如何开启多线程

开启方式

Java开启多线程有三种方式

继承Thread类
实现Runnable接口
java.util.concurrent.ThreadFactory 中的线程创建

继承Thread类

  • 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run方法称为线程执行体。
  • 创建Thread子类的实例,即创建了线程对象。
  • 调用线程对象的start()方法来启动该线程。切记是调用start()方法
    示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class ThreadTest {
    private int age = 10;
    public void threadTest(){
    for (int i = 0; i < 100; i++) {
    Thread thread = new NewThread();
    thread.start();
    }
    }
    class NewThread extends Thread{
    @Override
    public void run() {
    age++;
    System.out.println(age);
    }
    }
    }

输出日志

注意
通过下列匿名内部类的方式也可以开启多线程,本质也是实现了Runnable接口,并作为参数传递给Thread类,但是不推荐这样实现,因为在Android中,例如在Activity中使用匿名内部类方式,匿名内部类会持有外部类的引用,在Activity销毁时,如果线程执行未结束,导致Activity占用内存无法释放,造成内存泄漏。

1
2
3
4
5
6
7
8
9
/**
* 避免这种方式
/
new Thread(new Runnable() {
public void run() {
//执行体
}
}).start();

实现Runnable接口

  • 定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体
  • 创建Runnable实现类的实例对象,并以此实例对象作为Thread的target来创建Thread类,该Thread对象才是真正的线程对象。
  • 调用线程对象的start()方法来启动该线程。切记是调用start()方法
    示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class ThreadTest {
    private int age = 10;
    public void threadTest(){
    //实际的线程依然需要Thread实例对象,Thread才真正创建线程对象
    for (int i = 0; i < 100; i++) {
    Thread thread = new Thread(new ThreadRunnable());
    thread.start();
    }
    }
    class ThreadRunnable implements Runnable {
    @Override
    public void run() {
    age++;
    System.out.println(Thread.currentThread().getName() +"----"+ age); }
    }
    }

注意
Runable实现类里包含run方法,仅仅作为线程执行体,而实际的线程对象依然是Thread实例对象,Thread为真正创建线程的对象。

实现Runnable接口比继承Thread类所具有的优势:
  • 因为Java不支持多继承,但可以多实现,所以实现Runnable接口可以有效避免Java中单继承的限制。
  • 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
  • Java推荐使用组合而不是继承

java.util.concurrent.ThreadFactory 中的线程创建

java.util.concurrent 包中包含一个将线程创建抽象化的ThreadFactory 接口。利用该接口,我们可以将以Runnable 作为传入参数并通过new 创建Thread 实例的处理隐藏在ThreadFactory 内部。典型用法如下所示。默认的ThreadFactory 对象是通过Executors.defaultThreadFactory 方法获取的。

示例代码:

1
2
3
4
5
6
7
8
public void threadTest(){
for (int i = 0; i < 100; i++) {
ThreadFactory factory = Executors.defaultThreadFactory();
//此处的ThradRunnable是第二种方式中实现Runnable接口的类
factory.newThread(new ThreadRunnable()).start();
}
}

总结

上面罗列了三种启动多线程的方式,包括继承Thread类,实现Runnable接口,使用ThreadFactory线程创建,推荐使用第二种,毕竟组合优于继承

感谢

http://www.cnblogs.com/albertrui/p/8376790.html

Java进阶 ——— Java多线程(一)之进程和线程

引言

讲到线程,不可避免的提到进程。而因为线程无法脱离进程单独存在,那什么是进程?
延伸阅读,Java多线程系列文章

Java进阶 ——— Java多线程(一)之进程和线程
Java进阶 ——— Java多线程(二)之如何开启多线程

什么是进程?

进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的最小单位。
例如手机运行的众多APP,每个可以理解为一个进程(实际上很多APP运行多个进程),每个APP直接互相独立,互不干扰。

什么是线程?

线程:进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。也就是程序执行的最小单位。
例如优酷APP,我们看视频的同时还可以缓存视频。看视频和缓存视频就是运行在进程中的两个线程。

进程和线程的关系

进程是一个独立的空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

  • 简而言之,一个程序至少有一个进程,一个进程至少有一个线程
  • 线程的划分尺度小于进程,使得多线程程序的并发性高。
  • 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
  • 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

    举个不太准确的例子
    北京火车站负责北京到上海的铁路运输管理。北京火车站就是一个进程。这个进程负责整个线路的调度和分配。而北京到上海的线路,就是线程。这个线程是真正负责运送旅客和货物。

为什么要多线程?

现在要从北京运送大量旅客到上海,要在一天全部运送。如果只有一个线路,一辆火车去运送,那肯定是搞不定的,旅客在北京等的都疯了。

串行

那么是不是我可以加开多辆火车?然后依次送到上海?理论上是可以的。这种在一条线路上,所有火车依次行驶,就是串行,火车1、火车2、火车3依次行驶。只有火车1开动,火车2才能行驶。

那如果这个时候火车1出现故障,无法前进,该怎么办呢?或者铁路上只能同时开10辆火车,但是依然承载不了这么多旅客,一天时间还是运送不完?

并行

既然一条线路运送不了,那我开五条线路,每个线路都可以承载10辆火车,五个线路同时运行。不就可以满足了吗?

像上面这样,在一个进程中开启多个线程运行某个任务,就是多线程模式,多个线程同时运行,称为并行

如何开启多线程?

既然要开启多线程,该如何开启多线程呢?
在本篇文章,介绍了如何开启多线程Java进阶 ——— Java多线程(二)之如何开启多线程

多线程的安全问题

像上面这样,开启多个线程,同时运行,理论上是非常完美的解决办法。但是这个时候就会出现多线程的安全问题。

但是这时候每个线程接到通知:在天津站有也有不定量的旅客要乘车去上海。
1.第一条线路的第一辆火车到达天津,如果旅客全部上车了,后面的火车或者其他线路的火车不知道,到了天津站也停了,发现旅客全部上车走了…..
2.如果需要三辆火车才能接走全部天津站的旅客,那到底哪几条线路的哪几趟火车到天津站停车呢?
3.如果三辆车同时接近天津站,但是天津站站台每次只能停一辆火车,那谁先进站呢?

当然,这是假想的问题,实际开发工作中,多线程的问题远不止这些,下面用代码来演示多线程的安全问题。
开启100个线程同时修改成员变量age,并打印值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ThreadTest {
private int age = 10;
public void threadTest(){
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(new ThreadRunnable());
thread.start();
}
}
class ThreadRunnable implements Runnable {
@Override
public void run() {
age++;
System.out.println(age);
}
}
}

理想状态下,应该是线程依次增加age的值,从11增加到109。
来看下打印的结果

很明显,这个方法中线程并不是安全的,出现这种的原因有很多,最常见的原因就是,当某个线程1刚修改age的值,正要打印age的值,这时另一个线程2、3进入,又修改了age的值,并且打印结束,这时线程1才打印出age的值。

什么是线程安全呢?

简单理解:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的

或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。线程安全问题都是由全局变量及静态变量引起的

所以为了保证线程安全呢,就需要进行线程同步

什么是线程同步?

就是当一个程序对一个线程安全的方法或者语句进行访问的时候,其他的不能再对他进行操作了,必须等到这次访问结束以后才能对这个线程安全的方法进行访问

如何实现线程同步?

那么如何实现线程同步问题呢?下一篇文章我们继续了解如何实现线程同步?Java提供了哪些方式实现线程同步?

结束

本篇文章主要通过例子讲解什么是进程和线程,什么是多线程,什么是线程安全,什么是线程同步问题

感谢

https://www.cnblogs.com/lgk8023/p/6430592.html
https://mp.weixin.qq.com/s?__biz=MzI0OTQ4NzY5NA==&mid=2247484054&idx=1&sn=079565e4cda2298641f092f60033ace5&chksm=e9918ab7dee603a173476c4cd4f44a570aa31009a34f79615ba1fc508828cc0b33748b08fa4b&scene=21#wechat_redirect

https://www.cnblogs.com/duende99/p/7047067.html

Java进阶 ——— 局部内部类访问局部变量为什么必须加final关键字

疑问

在Java中,局部内部类如果调用了方法中的变量,那么该变量必须申明为final类型,如果不申明,则编译就会出错。

这里的内部类指的是方法内部类或匿名内部类,不包含静态内部类和成员内部类

这里通过一个例子类分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class InnerClass {
private int defaultAge = 5;
局部变量 age,必须添加final关键字,这里先不加
public void addAge( int age){
//局部内部类
class NewAge{
private int getAge(){
return age + defaultAge;
}
}
NewAge newAge = new NewAge();
System.out.print(newAge.getAge());
}
}

强行不加final,编译,则会报错:

Error:(16, 12) 错误: 从内部类中访问本地变量age; 需要被声明为最终类型

分析

  • 原因

    1.生命周期不同: 为什么必须局部变量加final关键字呢?因为局部变量直接存储在栈中,当方法执行结束,非final的局部变量就被销毁,而局部内部类对局部变量的引用依然存在,当局部内部类要调用局部变量时,就会出错,出现非法引用。简单来说,就是非final的局部变量的生命周期比局部内部类的生命周期短,是不是直接可以拷贝变量到局部内部类?这样内部类中就可以使用而且不担心生命周期问题呢?也是不可以的,因为直接拷贝又会出现第二个问题,就是数据不同步
    2.数据不同步:内部类并不是直接使用传递进来的参数,而是将传递进来的参数通过自己的构造器备份到自己内部,表面看是同一个变量,实际调用的是自己的属性而不是外部类方法的参数,如果在内部类中,修改了这些参数,并不会对外部变量产生影响,仅仅改变局部内部类中备份的参数。但是在外部调用时发现值并没有被修改,这种问题就会很尴尬,造成数据不同步。所以使用final避免数据不同步的问题
  • 原理

    那为什么添加final修饰的局部变量,就可以被局部内部类引用呢?
    若定义为final,则java编译器则会在内部类NewAge内生成一个外部变量的拷贝,而且可以既可以保证内部类可以引用外部属性,又能保证值的唯一性
    也就是拷贝了一个变量的副本,提供给局部内部类,这个副本的生命周期和局部内部类一样长,并且这个副本不可以修改,保证了数据的同步
    注意在Java8 中,被局部内部类引用的局部变量,默认添加final,所以不需要添加final关键词
  • 字节码

    如果有兴趣,可以看看编译后的字节码,即.class文件

    延伸
    如何查看Java字节码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class InnerClass$1NewAge {
    //可以看到,局部内部类中的使用的age,是通过构造函数传递进来,并不是直接引用外部变量。
    InnerClass$1NewAge(InnerClass var1, int var2) {
    this.this$0 = var1;
    this.val$age = var2;
    }
    private int getAge() {
    return this.val$age + InnerClass.access$000(this.this$0);
    }
    }

InnerClass类编译后,在文件夹会出现InnerClass.class和InnerClass$1NewAge.class,这说明外部类的方法 和内部类处于同一级。

结论

局部内部类引用局部变量,不添加final,会出现生命周期不同,导致非法引用问题,而且直接拷贝会出现数据不同步问题,所以使用final,保证了合法引用,而且数据不可修改

Java进阶 ——— 如何查看字节码

前言

深入学习Java语言,也免不了要去研究JVM类加载、JVM字节码相关的东西,这时就需要经常查看字节码。

方法

1.使用命令行

在编译后,进入字节码存放的文件夹,例如:FileA\Hello.class
使用命令行窗口
java -c FileA\Hello.class,即可在窗口浏览字节码文件

2.外接工具的方式

上面的描述编译成Hello.class文件(或者找到ide中相关out文件夹中的那些.class文件),然后下载Java Bytecode Editor,解压,直接运行jbe.sh即可看到UI面板,打开Hello.class文件即可看到类似下图的目录,将能看到非常丰富的类信息。

3.使用AndroidStudio 内置插件

在AndroidStudio 面板, AndroidStudio -> Preferences ->

Plugins ->jclasslib Bytecode Viewer -> Browse repositories -> install -restore AndroidStudio,重启AndroidStudio后,在选中要查看的.java文件, View -> Show bytecode with jclasslib,此时在ide的右边就会看到类似下图的界面,非常丰富的类信息

以上就是三种查看字节码的方法
参考:


https://blog.csdn.net/kwame211

Java进阶 ——— 面试必备 Java单链表反转

前言

关于链表操作,基本面试都会考察,每次都是看了就懂,过后就忘。看了
Java单链表反转文章,分析的很清晰,很好理解。

单链表的结点结构: 

  • data域:存储数据元素信息的域称为数据域; 
  • next域:存储直接后继位置的域称为指针域,它是存放结点的直接后继的地址(位置)的指针域(链域)。
  • data域+ next域:组成数据ai的存储映射,称为结点;
  • 注意:

    ①链表通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的。   

    ②每个结点只有一个链域的链表称为单链表(Single Linked List)。
    

    所谓的链表就好像火车车厢一样,从火车头开始,每一节车厢之后都连着后一节车厢。

要实现单链表存储,首先是创建一结点类,其Java代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Node {
private int Data;// 数据域
private Node Next;// 指针域
public Node(int Data) {
// super();
this.Data = Data;
}
public int getData() {
return Data;
}
public void setData(int Data) {
this.Data = Data;
}
public Node getNext() {
return Next;
}
public void setNext(Node Next) {
this.Next = Next;
}
}

实现反转的方法:

递归反转法

在反转当前节点之前先反转后续节点。这样从头结点开始,层层深入直到尾结点才开始反转指针域的指向。简单的说就是从尾结点开始,逆向反转各个结点的指针域指向,其过程图如下所示:

  • head:是前一结点的指针域(PS:前一结点的指针域指向当前结点)
  • head.getNext():是当前结点的指针域(PS:当前结点的指针域指向下一结点)
  • reHead:是反转后新链表的头结点(即原来单链表的尾结点)

Java代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class ReverseNode {
public static void main(String[] args) {
Node head = new Node(0);
Node node1 = new Node(1);
Node node2 = new Node(2);
Node node3 = new Node(3);
Node node4 = new Node(4);
Node node5 = new Node(5);
head.setNext(node1);
node1.setNext(node2);
node2.setNext(node3);
node3.setNext(node4);
node4.setNext(node5);
// 打印反转前的链表
Node h = head;
while (null != h) {
System.out.print(h.getData() + " ");
h = h.getNext();
}
// 调用反转方法
head = Reverse1(head);
System.out.println("\n**************************");
// 打印反转后的结果
while (null != head) {
System.out.print(head.getData() + " ");
head = head.getNext();
}
}
// 递归,在反转当前节点之前先反转后续节点
private static Node Reverse1(Node head) {
// head看作是前一结点,head.getNext()是当前结点,reHead是反转后新链表的头结点
if(null == head || head.getNext() == null){
//若为空链,或尾节点,则直接返回
return head;
}
//先反转后续节点
Node reHead = Reverse1(head.getNext());
head.getNext().setNext(head); //将当前节点的指针域指向上一节点
head.setNext(null); //前一节点的指针域改为null
return reHead; //返回反转后的新链表的节点
}
}

遍历反转法:

递归反转法是从后往前逆序反转指针域的指向,而遍历反转法是从前往后反转各个结点的指针域的指向。
基本思路是:将当前节点cur的下一个节点 cur.getNext()缓存到temp后,然后更改当前节点指针指向上一结点pre。也就是说在反转当前结点指针指向前,先把当前结点的指针域用tmp临时保存,以便下一次使用,其过程可表示如下:

  • pre:上一结点
  • cur: 当前结点
  • tmp: 临时结点,用于保存当前结点的指针域(即下一结点)

Java代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class ReverseNode {
public static void main(String[] args) {
Node head = new Node(0);
Node node1 = new Node(1);
Node node2 = new Node(2);
Node node3 = new Node(3);
Node node4 = new Node(4);
Node node5 = new Node(5);
head.setNext(node1);
node1.setNext(node2);
node2.setNext(node3);
node3.setNext(node4);
node4.setNext(node5);
// 打印反转前的链表
Node h = head;
while (null != h) {
System.out.print(h.getData() + " ");
h = h.getNext();
}
// 调用反转方法
head = Reverse2(head);
System.out.println("\n**************************");
// 打印反转后的结果
while (null != head) {
System.out.print(head.getData() + " ");
head = head.getNext();
}
}
//遍历,将当前节点的下一个节点缓存后更改当前节点指针
private static Node Reverse2(Node head) {
//若为空链,或尾节点,则直接返回
if(null == head ){
return head;
}
Node pre = head; //上一节点
Node cur = head.getNext(); //当前节点
Node temp; //临时节点,用于保存当前节点的指针域(即下一节点)
while (cur != null){ //如果为null,则是尾节点
temp = cur.getNext(); //下一节点
cur.setNext(pre); //将指针域指向上一节点
//节点继续向下移动
pre = cur;
cur = temp;
}
head.setNext(null); //最后将原链表的头结点指针域设置为null,此时原链表的头结点为新链表的尾节点
//此时pre已经移动到原链表的尾节点,也是新链表的头结点。
return pre;
}
}

上面就是两种方式反转单链表

  • 递归:先反转后续节点,再反转当前
  • 遍历:更改当前节点的指针域,指向上一节点,遍历执行

感谢

本文参考以下链接


https://blog.csdn.net/guyuealian/article/details/51119499

Android进阶 ——— Android 深入理解AIDL进程间通信

前言

关于AIDL的资料,层出不穷,但是能让人简单明了理解的文章不多,那么我们就自己撸一遍,清晰明了的理解一下AIDL的原理。

准备

在理解AIDL原理之前,先写一个简单的使用AIDL进行进程间通信的例子,根据这个例子来由浅及深的理解AIDL。

先来定义实体类:UserBean.java
注意:如果要在AIDL中使用实体类,实体类必须要实现序列化接口,这里实现的是Android自带的Parcelable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.t9.news.Model;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Created by dai
* Created time 2018/5/11
* function:com.t9.news.Model
*/
public class UserBean implements Parcelable{
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
protected UserBean(Parcel in) {
age = in.readInt();
name = in.readString();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(age);
dest.writeString(name);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<UserBean> CREATOR = new Creator<UserBean>() {
@Override
public UserBean createFromParcel(Parcel in) {
return new UserBean(in);
}
@Override
public UserBean[] newArray(int size) {
return new UserBean[size];
}
};
}

声明好了实体类,就需要额外在AIDL中声明实体类

1
2
3
4
// UserBean.aidl
package com.t9.news.Model;
parcelable UserBean;

在AIDL 中使用实体类

1
2
3
4
5
6
7
8
9
10
11
// IMyAidlInterface.aidl
package com.t9.news;
// Declare any non-default types here with import statements
import com.t9.news.Model.UserBean;
interface IUser {
List<UserBean> getUser();
void addUser(in UserBean user);
}

看一下项目的结构

然后Build 一下 Project,结束之后就生成了Binder代码,来看看Binder代码生成的位置

这时候可以点开看一下这个类,十有八九会被吓到,先不去管它,稍后再来分析。

来声明一个Service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.t9.news;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.util.Log;
import com.t9.news.Model.UserBean;
import java.util.ArrayList;
import java.util.List;
/**
* Created by dai
* Created time 2018/5/11
* function:com.t9.news
*/
public class MyService extends Service {
private String TAG = this.getClass().getName();
@Override
public void onCreate() {
super.onCreate();
}
List<UserBean> list = new ArrayList<>();
private IBinder binder = new IUser.Stub() {
@Override
public List<UserBean> getUser() throws RemoteException {
if (list.size() <= 0){
for(int i = 0; i < 5; i++){
UserBean bean = new UserBean();
bean.setAge(5 * i);
bean.setName("android-" + i);
list.add(bean);
}
}
return list;
}
@Override
public void addUser(UserBean user) throws RemoteException {
Log.e(TAG, user.getName());
list.add(user);
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
return binder;
}
}

定义的Service比较简单。new 了一个 IUser.Stub()并把它向上转型成了IBinder,最后在onBind方法中返回回去。在 IUser.Stub()的内部我们重写getUser()、addUser(UserBean user)方法,这就是AIDL中声明的IUser接口中的两个方法。
既然是跨进程通信,那么将Service设置到另一个进程中:

1
2
3
4
<service
android:name="com.t9.news.MyService"
android:process=":newProcess"
/>

定义为启动在新进程中,只需要在AndroidMainfest.xml中声明是加上一个process属性即可,不过这里有两个地方值得注意:
1.组件默认的进程名就是包名;
2.定义新的进程名的时候需要以包的形式(eg: com.xu.aidl)。

好,准备工作完成,在Activity中绑定Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.t9.News.HomePage.View.Activity
import android.Manifest
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.RemoteException
import android.util.Log
import com.t9.news.Application
import com.t9.news.R
import com.t9.news.IUser
import com.t9.news.Model.UserBean
import com.t9.news.MyService
/**
* Created by dai
* Created time 19:06
* function:com.t9.news.Main.View.Activity
*/
class MainActivity : BaseActivity() {
private val TAG = this.javaClass.simpleName;
override fun getLayoutId(): Int {
return R.layout.activity_main
}
override fun initView() {
initService()
}
fun initService(){
val service = Intent(this@MainActivity, MyService::class.java)
bindService(service, serviceConnection, Context.BIND_AUTO_CREATE)
}
private var iUser : IUser? = null
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
iUser = IUser.Stub.asInterface(service)
Log.e(TAG,"连接Service成功")
try {
val list : List<UserBean> = iUser!!.user
for (user in list){
Log.e(TAG, "name = " + user.name + " age: = " + user.age)
}
val user:UserBean = UserBean();
user.name = "张三"
user.age = 111
iUser!!.addUser(user)
} catch (e: RemoteException) {
e.printStackTrace()
}
}
override fun onServiceDisconnected(name: ComponentName) {
Log.e(TAG,"连接Service断开")
}
}
}

由于主要是Service和Activity间的通信,所以为了让代码整洁就没有写UI了。

在onCreate(Bundle savedInstanceState)中,我们调用了自己定义的一个方法initService(),这个方法里面我们生成了一个Intent,然后 bindService了这个Intent传入了三个参数分别是Intent、ServiceConnection、Flag。

Intent我们就不用说了,我们看看后面两个参数:
在Activity中,我们new了一个ServiceConnection并实现了他的两个方法onServiceConnected、onServiceDisconnected。在onServiceConnected中我们通过IUser.Stub.asInterface(service)把传来的IBinder转换成了我们定义的iUser。然后我们调用了getUser方法,传递了个字符串和获取从MyService传来的字符串,并且打印了Log。
然后又new 一个UserBean对象,传递给MyService。

然后,我们的编码就完成了,运行并观察Log:

  • MainActivity 中 Log:

  • MyService中Log:

根据运行结果,在这两个不同的进程中都得到了我们想要的结果,所以,一个用aidl实现的跨进程通信就这样完成了。

AIDL的理解

回过头来,我们再来分析之前的 debug 目录下的 IUser.java 类

  • 先来看MyService 中 Binder

还记得我们在MyService中利用new IUser.Stub()向上转型成了IBinder然后在onBind方法中返回的。那我们就看看IUser.Stub()吧:

1
2
3
public static abstract class Stub extends android.os.Binder implements com.t9.news.IUser{
......
}

Stub 是 IUser 中静态抽象类,继承了 Binder,并且 实现 IUser接口,这就说明我们定义IUser.Stub的时候为什么需要实现IUser中的方法了,也说明了为什么我们可以把IUser.Stub向上转型成IBinder了。

  • Activity中的IMyInterface
    在Activity中,通过ServiceConnection连接MyService并成功回调onServiceConnected中我们把传回来的IBinder通过IUser.Stub.asInterface(service)转换成为IUser,那就来看看这里是如何转换的吧:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    //这个接口里 有一个静态的抽象类Stub(注意这个名字是固定的 永远都是Stub 不会是其他)
    //并且这个Stub是Binder的子类,并且实现了IUser 这个接口
    public static abstract class Stub extends android.os.Binder implements com.t9.news.IUser{
    //这个东西就是唯一的binder标示 可以看到就是IUser的全路径名
    private static final java.lang.String DESCRIPTOR = "com.t9.news.IUser";
    /** Construct the stub at attach it to the interface. */
    /**
    * 这个就是Stub的构造方法,回顾一下 我们如果写好aidl文件以后 写的service里面 是怎么写的?
    * private final IUser.Stub mBinder = new IUser.Stub() {}
    * 我们都是这么写的 对吧~~所以想想我们的service里面的代码 就能辅助理解 这里的代码了
    */
    public Stub()
    {
    this.attachInterface(this, DESCRIPTOR);
    }
    //这个方法 其实就做了一件事,如果是同一个进程,那么就返回Stub对象本身
    //如果不是同一个进程,就返回Stub.Proxy这个代理对象了
    public static com.t9.news.IUser asInterface(android.os.IBinder obj){
    if ((obj==null)) {
    return null;
    }
    //检查Binder是不是在当前进程
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    //如果是同1个进程,也就是说进程内通信的话 我们就返回括号内里的对象
    if (((iin!=null)&&(iin instanceof com.t9.news.IUser))) {
    return ((com.t9.news.IUser)iin);
    }
    //如果不是同一进程,是2个进程之间相互通信,那我们就得返回这个Stub.Proxy 看上去叫Stub 代理的对象了
    return new com.t9.news.IUser.Stub.Proxy(obj);
    }
    }

首先,我们因该明白的是,传回来的IBinder就是我们在Service的onBind( )方法所return的IBinder,然后我们调用Stub中的静态方法asInterface并把返回来的IBinder当参数传进去。
在asInterface方法中,首先判断了传进来的IBinder是不是null,如果为null就返回一个null;接着就判断传进来的IBinder是不是就在当前进程里面,如果是的话就直接返回IUser,不是的话就返回IUser.Stub.Proxy(obj)。
这里我觉得需要明白的是:直接返回的IUser是实现了定义的接口方法getUser、AddUser的。因为在IUser.Stub中所实现的。当然如果是在同一进程中,那么我们调用IUser的方法时就是在本地调用方法,直接调用就可以了。

如果没在同一进程,就会返回IUser.Stub.Proxy(obj):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//注意这里的Proxy 这个类名也是不变的,从前文我们知道 只有在多进程通信的情况下 才会返回这个代理的对象
private static class Proxy implements com.t9.news.IUser{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote){
mRemote = remote;
}
@Override public android.os.IBinder asBinder(){
return mRemote;
}
public java.lang.String getInterfaceDescriptor(){
return DESCRIPTOR;
}
//这里我们一共有2个方法 一个getUser 一个addUser 我们就分析一个方法就可以了
//并且要知道 这2个方法运行在客户端!!!!!!!!!!!!!!!!
//首先就是创建了3个对象_data 输入对象,_reply输出对象,_result返回值对象
//然后把参数信息 写入到_data里,接着就调用了transact这个方法 来发送rpc请求,然后接着
//当前线程挂起, 服务端的onTransace方法才被调用,调用结束以后 当前线程继续执行,直到
//从_reply中取出rpc的返回结果 然后返回_reply的数据
//所以这里我们就要注意了,客户端发起调用远程请求时,当前客户端的线程就会被挂起了,
//所以如果一个远程方法 很耗时,我们客户端就一定不能在ui main线程里在发起这个rpc请求,不然就anr了。
@Override public java.util.List<com.t9.news.Model.UserBean> getUser() throws android.os.RemoteException{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.t9.news.Model.UserBean> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
//传送数据到服务端
mRemote.transact(Stub.TRANSACTION_getUser, _data, _reply, 0);
_reply.readException();
//接受从服务端传回的数据
_result = _reply.createTypedArrayList(com.t9.news.Model.UserBean.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override public void addUser(com.t9.news.Model.UserBean user) throws android.os.RemoteException{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((user!=null)) {
_data.writeInt(1);
user.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addUser, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
}

在Proxy中,我们首先把Service连接成功返回的IBinder它的内部变量mRemote,这里在提一下,这里得IBinder还是是MyService中onBind所返回的。然后,当我们调用IMyInterface的方法的时候,其实就是调用的Proxy的方法了,这也是为什么这个类叫做Porxy的原因了。

当调用IUser.getUser() ,我们就看Proxy中的getInfor,先获取了两个Parcel对象 _data、_data,从变量名就可以看出,一个是传送数据的,另一个则是接受返回数据的。接着,向_data中写入了DESCRIPTOR(也就是这个类的全名),再写入了方法参数。然后就到了最重要的一步了,

mRemote.transact(Stub.TRANSACTION_getInfor, _data, _reply, 0);
这里我们调用了IBinder的transact方法,来把数据传给远端的服务器。然后在我们远程的MyService中,里面的Stub中就会回调onTransact()(因为你把数据传个远程的服务,远端的服务收到数据也就回调了)

注意:这里是在远程的服务里调用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//只有在多进程通信的时候 才会调用这个方法 ,同一个进程是不会调用的。
//首先 我们要明白 这个方法 一般情况下 都是返回true的,也只有返回true的时候才有意义,如果返回false了 就代表这个方法执行失败,
//所以我们通常是用这个方法来做权限认证的,其实也很好理解,既然是多进程通信,那么我们服务端的进程当然不希望谁都能过来调用
//所以权限认证是必须的,关于权限认证的代码 以后我再讲 先略过。
//除此之外 ,onTransact 这个方法 就是运行在Binder线程池中的,一般就是客户端发起请求,然后android底层代码把这个客户端发起的
//请求 封装成3个参数 来调用这个onTransact方法,第一个参数code 就代表客户端想要调用服务端 方法的 标志位。
//其实也很好理解 服务端可能有n个方法 每个方法 都有一个对应的int值来代表,这个code就是这个int值,用来标示客户端想调用的服务端的方法
//data就是方法参数,reply就是方法返回值。都很好理解
//其实隐藏了很重要的一点,这个方法既然是运行在binder线程池中的,所以在这个方法里面调用的服务器方法也是运行在Binder线程池中的,
//所以我们要记得 如果你的服务端程序 有可能和多个客户端相联的话,你方法里使用的那些参数 必须要是支持异步的,否则的话
//值就会错乱了!这点一定要记住!结论就是Binder方法 一定要是同步方法!!!!!!
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException{
switch (code){
case INTERFACE_TRANSACTION:{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getUser:{
data.enforceInterface(DESCRIPTOR);
// 远程服务调用自己本地实现的方法获取返回值
java.util.List<com.t9.news.Model.UserBean> _result = this.getUser();
reply.writeNoException();
//写入返回值
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addUser:{
data.enforceInterface(DESCRIPTOR);
com.t9.news.Model.UserBean _arg0;
if ((0!=data.readInt())) {
_arg0 = com.t9.news.Model.UserBean.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addUser(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}

nTransact方法是在Stub的内部实现的。

先看一下它的四个参数:

  • code:每个方法都有一个int类型的数字用来区分(后面中的swicth),在我们例子中也就是我们Proxy中的Stub.TRANSACTION_geIUser。
  • data:传过来的数据,其中包含我们的参数,以及类的描述。
  • reply:传回的数据,我们要写入是否发生了Exception,以及返回值
  • flags:该方法是否有返回值 ,0表示有返回值。

调用onTransact就表示有数据传来,首先就会通过swicth判断是哪个方法,然后取出方法参数,调用本地实现的方法获取返回值,写入返回值到reply。最后,返回true,才会把数据发送出去,发挥false就不会把结果返回给Activity了。这里也就是说,只有返回true,我们Proxy中才能接受从远端传回的数据。

注意:Service也是把数据发送出来,让客户端接受的。

Service发出了数据,客户端接收到了,就会一层一层返回去。所以,当我们简单的调用IUser的getUser时候,先是Proxy的transact发送出数据,然后服务端的onTransact接受并处理传来的数据,再把处理得到的数据写入返回值并发送给客户端,客户端读取值后就成为调用方法的返回值返回了。

到这里 相信大家 至少在应用层上面,就对Binder就一个很直观的理解了,对于进程间通信来说,具体的流程就分为如下几步:

  • 1.Client 发起远程调用请求 也就是RPC 到Binder。同时将自己挂起,挂起的原因是要等待RPC调用结束以后返回的结果

  • 2.Binder 收到RPC请求以后 把参数收集一下,调用transact方法,把RPC请求转发给service端。

  • 3.service端 收到rpc请求以后 就去线程池里 找一个空闲的线程去走service端的 onTransact方法 ,实际上也就是真正在运行service端的 方法了,等方法运行结束 就把结果 写回到binder中。

  • 4.Binder 收到返回数据以后 就唤醒原来的Client 线程,返回结果。至此,一次进程间通信 的过程就结束了

关于AIDL的基本原理就是这样了,看明白了AIDL,才发现原来AIDL不过就是帮我们生成了那些数据写入,传送,读取的方法而已。


参考
https://www.cnblogs.com/punkisnotdead/p/5163464.html
https://blog.csdn.net/u011974987/article/details/51243539

Android 进阶 ------ Android 单元测试

什么是单元测试?

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。比如我们可以测试一个类,或者一个类中的一个方法。

为什么要进行单元测试?

为什么要进行单元测试?说白了就是单元测试有什么好处,其实测试的好处无非就是减少bug、提高代码质量、使代码易于维护等。

这样表达可能不太好理解,举个例子

比如我们APP启动,从初始化到启动页,再进入首页,需要很长的流程,如果主页需要请求数据,每次都要启动APP,经过一系列流程进入主页,才能测试主页请求数据模块,效率低下。
当然我们也能在初始化时请求数据,但是这样需要更改代码逻辑,同样需要启动APP,而如果用单元测试,我们可以直接测试请求数据模块,不需要任何逻辑修改。提高效率

开始进行单元测试

AS创建工程时,src下创建 androidTest 和 test 目录,这两个目录有什么区别呢?

src/test src/androidTest
位于src/tests目录下的测试是运行在本地电脑Java虚拟机上的单元测试。编写测试,实现功能使测试通过,然后再添加更多的测试…这种工作方式使快速迭代成为可能,我们称之为测试驱动开发 跟src/test不同的是运行在设备上,并充分利用Android框架的测试

简单来说,src/test不需要你连接设备,而src/androidTest需要连接设备测试。

一、先说一下src/tests

首先确认app下gradle中存在依赖:

1
2
3
dependencies {
testCompile 'junit:junit:4.12'
}

这里有个 快速排序的类,对一个数组进行快速排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class FastSort {
public void main(){
int[] a = new int[]{5,10,34,2,45,38,23,70,1,32,20,17,4,80,14,100};
quick_sort(a,0,a.length - 1);
for (int i : a) {
System.out.println(i);
}
}
void quick_sort(int a[],int l,int r){
if (l < r){
int i = adjustArray(a,l,r);
quick_sort(a,l,i-1);
quick_sort(a,i+1,r);
}
}
private int adjustArray(int a[],int l,int r){
int i = l,j = r,x = a[i];
while (i < j){
while (i < j && a[j] >= x){ //如果 a[j] > x ,不用管,j-1,倒着比较下一个
j--;
}
if (i < j){ // a[j] < x 跳出while循环
a[i] = a[j]; //将a[j]的值赋给a[i]被挖出的位置,默认是数组都一个
i++; //i++,为后面i开始自增,提前增加
}
while (i < j && a[i] < x){ //如果 a[i] < x ,不用管,x+1,比较下一个
i++;
}
if (i < j){ // a[i] > x 跳出while循环
a[j] = a[i]; //将a[i]的值赋给上面a[j]被挖出的位置
j--; //j--,为后面j继续自减,提前减少
}
}
a[i] = x;
return i;
}
}

既然工具类写好了,当然要测试一下排序方法对不对了,这时候直接用单元测试来检验一下。

  • AS为我们提供了创建测试类的快捷方法:

这里写图片描述

  • 右键 –> Go To –> Test
    这里写图片描述

  • 勾选上所有方法,点击 OK,一路点击OK,完成测试类创建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class FastSortTest extends TestCase {
    public void setUp() throws Exception {
    super.setUp();
    }
    public void tearDown() throws Exception {
    }
    public void testMain() throws Exception {
    }
    public void testQuick_sort() throws Exception {
    }
    }

setUp:在测试开始之前回调的方法( 比如说初始化我们的类)。
tearDown():测试结束的回调方法。

  • 来添加测试代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class FastSortTest extends TestCase {
    private FastSort sort;
    public void setUp() throws Exception {
    super.setUp();
    sort = new FastSort();
    }
    public void tearDown() throws Exception {
    }
    public void testMain() throws Exception {
    sort.main();
    }
    public void testQuick_sort() throws Exception {
    }
    }

sort.main()方法,检测快速排序方法的正确性

  • 开始测试
    右键点击FastSortTest类 —> ‘Run FastSortTest’,也可以通过命令行运行测试,在工程目录内输入:
    1
    ./gradlew test

这里写图片描述

测试结束,可以看到,sortMain()方法通过测试
这里写图片描述

成功完成了 src/test 单元测试,


这里简单介绍了 src/test 测试,关于其他用法,大家可以去深入研究一下

|