Java多线程和同步的理解

进程与线程

在谈论线程之前,我们先来看看什么叫进程,以及进程与线程的关系。

进程

我们在windows操作系统中打开任务管理器,可以看到有一项是“进程”,里面列举出了用户目前正在运行的所有进程,包括系统进程和用户应用程序进程,以及每个进程所占用的内存资源等信息。进程是操作系统结构的基础,它不仅只包括运行的程序代码,还包括当前的活动。对于每一个进程,操作系统都会为其分配一个独立的内存块,各进程间资源是不共享的。
划分时间片,宏观上并行,微观上串行

线程

一个Java程序运行之后,就会启动一个JVM实例进程,这个进程就负责处理这个程序所有的操作,直到程序结束,进程也随之结束。
而线程就是再在进程的内部将CPU资源进行再次划分,以满足同时处理多条语句的需要(微观上,其实也是并行执行的),这些线程在进程内部的资源是共享的(正因如此,才会有同步以及锁的出现)。
JVM进程启动一定会有一个主线程存在,即main方法启动的线程,这个线程是Java程序的入口,我们可以在main方法内部在定义我们自己的线程,这样就可以实现多线程了。

Java多线程的实现方式

java.lang.Thread类的一个对象就代表一个线程
线程是底层OS(操作系统)维护的资源,JVM跑在OS上,在JVM中创建一个Thread对象,调用其start()方法,底层OS会申请一个线程资源,线程对象可到底层管理一个线程,创建好线程之后,把要让线程执行的代码封装到线程对象中(覆盖run()方法)。
实现线程代码的方式:

  1. 继承Thread类,覆盖run()方法
    去底层申请线程并运行,对线程对象调start()方法,main方法是一个主线程
    宏观并行,微观串行

  2. 实现Runnable接口
    使用多态获得Runnable对象,成为目标对象
    再利用目标对象构造线程对象Thread t = new Thread(target);//target为Runnable接口类型
    对于中两种方法的具体介绍可以参考:
    http://hi.baidu.com/hi_place/blog/item/84dcf8f283d4f005b17ec51f.html

线程的优先级

线程的优先级是从0-10的整数,0表示最低,5表示普通,10表示最大;JVM会自动将java线程的优先级转换为操作系统的优先级。
main线程的优先级是5。

线程的状态

下面为线程中的7个非常重要的状态(有的书上也只有认为前五种状态:而将“锁池”和“等待池”都看成是“阻塞”状态的特殊情况:这种认识也是正确的,但是将“锁池”和“等待池”单独分离出来有利于对程序的理解):

  1. 初始状态:线程刚创建(Thread th = new Thread(target);)
  2. 可运行状态:线程创建之后调用它的start()方法,此时线程状态就变更为可运行状态,但一定就会立即运行,需要等待获得CPU。
  3. 运行状态:调用线程的start()方法之后,线程就会进入等待运行状态(可运行状态),此时一旦该线程获得CPU的使用权,县城就会立即进入运行状态,即执行线程的run()方法。
  4. 阻塞状态:线程失去CPU的使用权,进入一种等待状态,注意不是可运行状态。有以下三种情况会使线程进入阻塞状态:
    4.1 待外部设备输入:如等待键盘输入,则该线程会进入阻塞状态直到输入完毕,注意:阻塞结束之后是进入可运行状态,而不是运行状态。
    4.2 程休眠,即调用线程的sleep()方法。Sleep()方法有一个参数,表示休眠的时间,当线程休眠的时间到达指定时间后,线程会自动结束阻塞状态而进入可运行状态,等待CPU。
    4.3 一个线程调用另一个线程的join()方法,join()方法指的是调用该方法的线程将进入阻塞状态直到被调用join()方法的线程运行结束之后,才会进入可运行状态。
    例:在t2线程的run()方法内部有这样一句代码t1.join();(t1是一个线程对象),这将意味着党线程t2执行到该语句时就会调用线程t1的join()方法,从而t2进入阻塞状态,直到t1运行结束为止。
  5. 终止状态:即线程执行结束
  6. 锁池状态
  7. 等待队列

线程的同步

之前说过同意进程中的各线程之间是资源共享的,这种资源共享的特性会带来的后果就是线程同步的问题。
下面看一个例子:

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
publicclass Test {
publicstaticvoid main(String args[]) {
Thread1 th = new Thread1();
Thread t1 = new Thread(th);
Thread t2 = new Thread(th);
Thread t3 = new Thread(th);
Thread t4 = new Thread(th);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Thread1implements Runnable {
privateinttickets = 100;
@Override
publicvoid run() {

while (tickets > 0) {
try {
Thread.sleep(1000);//休眠1s
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +":"
+ tickets--);
}
}
}

这个程序打印出来的结果绝对不是我们想要的。这就是多线程同时操作一个数据会出现的读脏数据等现象。为避免出现这种情况就需要使用到Java线程的同步机制,即锁机制。

Java实现同步有两种方法:Synchronized和Lock。
多线程同时并发访问的资源叫做临界资源。多个线程同时访问对象并要求操作相同资源时分割了原子操作就会出现问题。(原子操作,不可再分的操作)会出现数据的不一致或数据不完整,为避免这种现象采用对访问的线程做限制的方法。

Synchronized用法
互斥锁机制,利用每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程。
(1) Synchronized修饰代码块(同步代码块)

1
2
3
4
5
6
public void push(char c){
synchronized(this)//只有持有当前对象锁标记的线程才能访问这个代码块
{
...
}
}

此时Synchronized锁的是调用该方法的对象(this),也可以选择锁住任何一个对象。
(2) Synchronized修饰方法(同步方法)

1
2
3
public synchronized void push(char c) {
...
}

此时Synchronized锁的是调用该方法的对象,同上面的this。
说明:
A、无论Synchronized加在方法上还是对象上,它取得锁都是对象,而不是把一段代码或者函数当做锁——而且同步方法还可能会被其他线程的对象访问。
B、每个对象只有一个锁与之相关联。
C、实现同步需要很大的系统开销作为代价,甚至可能造成死锁,所以尽量避免无谓的同步控制。
接下来继续讨论Synchronized加在方法上和加在对象上两者的区别
Synchronized加在对象上:

1
2
3
4
5
6
public void push(char c){
synchronized(this)//只有持有当前对象锁标记的线程才能访问这个代码块
{
...
}
}

其中的this也可以是任何其他对象,而synchronized所起的作用就是锁住括号中的对象,使得当其他线程需要调用该方法时由于无法取得对括号中的对象的锁而无法继续执行,而进入锁池。
线程因为未拿到锁标记而发生阻塞进入锁池(lock pool)。每个对象都有自己的一个锁池的空间,用于放置等待运行的线程。由系统决定哪个线程拿到锁标记并运行。
当有一个明确的对象可以用来锁时,我们直接将这个对象锁起来即可,但是当没有一个明确的对象可以用来加锁时,我们可以创建一个对象来充当锁。

Synchronized加在方法上:

1
2
3
public synchronized void push(char c) {
...
}

此时synchronized锁住的是当前调用该方法的对象。若p1调用该方法,此p1这个对象会被锁住,那么p1在不同线程中执行该方法时会形成互斥。但是另一个对象p2却可以人任意调用该方法。

Synchronized加在静态方法上:
我们知道静态方法是一个class所拥有的公共方法,由这个class实例化得到的所有的对象都具有对这个方法的访问权,因此我们可以想象如果将它锁住,那么其他对象是不是也不能够调用这个方法了呢?答案是是的。
public synchronized static void push(char c) {

}
对静态方法加锁,锁住的是包含这个静态方法的Class类,即字节码,因此由这份字节码生成的所有对象都将无法调用该方法。

Lock的用法
java.util.concurrent.locks中的Lock接口,有多个实现类,调用lock和unlock方法,实现和synchronized一样的功能;
ReadWriteLock接口,有两个获得锁的方法,读锁和写锁。
读写锁特征:

  • 如果读锁被一个线程锁住了,则其他线程可以再锁读锁,但不允许写锁
    一个线程读操作,也允许其他线程做读操作,但不允许做写操作
  • 如果写锁被一个线程锁住了,则其他线程读锁和写锁都不可以锁锁
    一个线程写操作,其他线程读写操作都不可以。