多线程较好地实现了提升程序运行多部分代码同时运行的效率。但是需要注意,多线程在执行“任务”是存在着“安全问题”。
1 线程的运行状态
1.1 被创建:通过new Thread或其子类,线程被创建。
1.2 运行:线程start()后开始运行,执行run()里的任务。
1.3 消亡:即线程消失。线程被stop()处理或run()运行完毕后,线程会消亡。
1.4 冻结:可以理解为线程被暂停了。一般用sleep(时间)或wait()让线程暂停。此时线程放弃了运行资格,不允许系统去调用它。
1.5 阻塞:这是线程的一个临时状态,是一个特别的存在形态。意思是该线程虽然被语句赋予(或恢复)了运行的资格,但未有运行权,直到系统调用它。简单的说就是线程希望活动,只等被系统调用时的状态。实际上,线程被start()运行后并不是真的马上就被调用,而是去到阻塞状态,等待调用,系统调用到它,它才算真正地开始执行语句。多线程运作的时候,每个线程就是这样在阻塞状态与运行状态来回游走,但是系统具体每个时刻调用谁是无规律的。所以这个过程也可以理解为线程在抢系统资源,谁抢到了谁就执行自己的语句。
同样,当线程从冻结状态中恢复(即sleep时间结束了或wait后被notify叫起),也是先回到阻塞状态,再”抢资源“。
线程的各种存在状态如下简图:
2. 多线程的“安全问题”的要素
①多个线程在操作共享的数据
②操作共享数据的线程代码有多条
③当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算。—— 一旦出现了这样的情况,就产生安全问题。
例子:3个线程同时数数,从100倒数到1。
class Demo implements Runnable {
private int num = 100;
public void run(){
while (true){
if (num>0){
try {
Thread.sleep(100); //设置sleep语句是为了故意创造多线程问题的条件。该语句产生InterruptedException异常
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
<span style="white-space:pre"> </span>//throw e; //因为父类Runnable中该错误没有抛出,只能try catch,
}
System.out.println(Thread.currentThread().getName()+"...count..."+num--);
}
}
}
}
class Day13{
public static void main(String[] args){
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
Thread t3 = new Thread(d);
t1.start();
t2.start();
t3.start();
}
}
结果输出了带0,-1的结果,明显是不符合程序的实现的。
结合程序分析该多线程出现安全问题的原因:
①多个线程在操作共享的数据:
因为程序只创建了1个Demo实例对象,所以实际上Demo里的int num成员是被3个线程共享的。3个线程执行run任务的时候指向的都是同一个int num。
②操作共享数据的线程代码有多条:
即if判断语句和后面的输出语句,都需要调用num。
③当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算:
即对上图红框的解释:在本程序里,其中一个线程在if(num>0)里用num=1判断为真,进入,但在sleep(100)里进入了冻结状态,丢失了运行资格。
这时程序调用另一个线程在if(num>0)同样用num=1(因为此时Thread-2并没有执行到num--)判断为真,进入。同理,丢失资格。
第三个线程同理,在sleep(100)语句中冻结。
冻结结束后,Thread-1重获运行资格并率先被赋予运行权,执行接下来输出语句,得到Thread-1...count...1,同时num--,num=0
Thread-2随后被赋予运行权,输出语句,因为此时num已经是0,所以得到Thread-2...count...0,同时num--,num=-1
最后Thread-0运行,输出Thread-0...count...-1
简单的说,就是一个线程在执行多条多线程代码调用共享数据途中被其它线程“插队”,导致后续数据产生错乱。
3.安全问题的解决——同步锁
要解决该问题,可行的办法是将那些调用共享数据的代码封装起来,并规定在一个线程执行的时候,其它线程不能再进入该封装。如果该线程在执行过程中冻结了,其它线程只能在封装外等待。直到该线程执行完成。
3.1 java就给了一个专门处理这些问题的方法——同步代码块
synchronized(对象){访问共享数据的代码}
其中:该”对象“可以称为该同步的锁。是任意一个类的对象。
上个例子进行改良:
class Demo implements Runnable {
Object obj = new Object();
private int num = 100 ;
public void run(){
while (true){
synchronized(obj){
if (num>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
// throw e;
}
System.out.println(Thread.currentThread().getName()+"...count..."+num--);
}}
}
}
}
class Day13{
public static void main(String[] args){
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
Thread t3 = new Thread(d);
t1.start();
t2.start();
t3.start();
}
}
无论运行多少次,都不会再出现之前的安全问题。因为操作num的多个代码已经被同步了。
synchronized的运作原理是:给“任务”加上锁,所有的线程只有读取到synchronized拿到锁后才能执行”任务“,所有语句结束后,释放锁,允许其它线程进入。由于这个锁有一开一关的动作,可以粗略地理解为0和1,线程读取synchronized后将锁(即对象)置为1,并由synchronized规定得到1后不允许其它线程再获取锁。直到正在执行的线程执行完毕,将锁置为0。
3.2 同步锁运用对程序编写的影响
以下程序演示两个线程共同对100进行倒数到1。
class Demo extends Thread{
Object obj = new Object();
private static int i = 100;
public void run(){
for(; i>0;i--){
synchronized (obj){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
}
}
public class Day13{
public static void main(String[] args){
Demo t1 = new Demo();
Demo t2 = new Demo();
t1.start();
t2.start();
}
}
输出结果:同步失败。生成了如Thread-1...0这样的输出。
原因分析:
①为了实现多线程,使用了Demo继承Thread的方法,这样导致一个隐患:我们每次创建线程都相当于创建一次Demo,注意此时Demo里的各种成员都被创建一次,除非静态。此时,虽然在synchronized上用了Object obj作为锁,但是创建了两个线程之后,每个线程实际上各自拿着一个自己的对象里的锁。这样相当于各自有一个房间,各自拿着各自的锁,互不想干。
②因为for语句上含有i>0的判断以及后来的i--操作,多条语句操作了int i这个共享数据,实际上for语句从一开始就应该被同同步,但这里没有。
解决方案:
①尝试把锁obj静态化,让各个线程们被“同一个锁”控制。
②把for语句整个同步。
得到如下:
class Demo extends Thread{
<span style="color:#ff0000;">static </span>Object obj = new Object(); <span style="color:#ff0000;">//让每个线程的锁指向同一个对象,被“同一个锁”控制</span>
private static int i = 100;
public void run(){
<pre name="code" class="java"> <span style="color:#ff0000;">synchronized (obj){ //因为for循环调用了i,把整个for循环同步</span>
for(; i>0;i--){
// synchronized (obj){try {Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
}
输出结果:不再出现记数为0的错误,但是发现整个倒数只被1个线程执行了。另一条线程虽然启动了,但并未执行到。
原因分析:
这里是同步出了问题:从读取synchronized后开始,线程一直在for循环里读取,全输出完了。这样就成了单线程。
解决方案:
也就是说要把处理的数据提取出来,但循环本身不要进入同步。这样才能允许各程序”抢资源“进入同步,参与循环,实现多线程。
程序修改如下。
class Demo extends Thread{
static Object obj = new Object(); //让每个线程的锁指向同一个对象,被“同一个锁”控制
private static int i = 100;
public void run(){
<span style="white-space:pre"></span><pre name="code" class="java"><span style="white-space:pre"> </span>for(; ;){
synchronized (obj){ //因为for循环调用了i,把整个for循环同步
if(i>0){ //把判断独立// synchronized (obj){try {Thread.sleep(10);
<pre name="code" class="java"> } catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"..."+i);
}
<span style="white-space:pre"> </span><span style="color:#ff0000;">i--; //把递进独立</span>
}
}}
......
总结:同步的两个前提:
①必须要有多个线程;
②需要同步处理的多个线程必须使用同一个锁。