并发系列一之初识java线程

前言

曾在论坛中看过这样一句话,如果连多线程都不懂,别说自己学过java,可见多线程的重要性!

何谓线程

简单来说,就像我们在执行一个程序时,他总按照顺序执行,如果从头到尾都没有分叉,也就是说总是一个流程,那么就可以看做单线程,并且程序执行的主体只有一个,所以,我们把正在执行程序的主体称为线程

当然,平时我们说的最多的就是多线程,顾名思义就是由一个以上的线程所构成的程序,比如文件处理,io请求处理及网络客户端发出的请求等等都可以看做多线程

Thread类

先看一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
package com.zwl.utest1;
import java.util.Date;
public class Mythread extends Thread {
@Override
public void run() {
for(int i=0;i<700;i++){
System.out.print("hello!");
}}}

测试类

1
2
3
4
5
Mythread m=new Mythread();
m.start();
for(int i=0;i<900;i++){
System.out.print("lol!");
}

测试结果

lol!lol!lol!lol!lol!lol!。。hello!hello!hello!hello!hello!。。lol!lol!lol!lol!lol!lol!

然后在去认识上面程序,其中start方法会启动新的线程,然后再由这个新线程调用run方法,
当然也可以直接调用run方法

那么两者有什么区别了?
调用start方法会出现字符串交错,也就是并发,而run方法,就是等run方法执行完,在执行其它,显然不会出现交错现象
当然数据量小的话,出现的结果很难有说服力,所以并发模拟尽量使用大数量

线程的启动

  1. 利用thread的子类的实例,启动线程
    正如前面模拟的程序,不过下面测试用两个线程
    1
    2
    new Mythread().start();
    new Mythread().start();

显然两个线程操作两个对象,所以thread是线程安全的

  1. 利用Runnable接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Mythread implements Runnable
    {
    @Override
    public void run() {
    for(int i=0;i<700;i++){
    System.out.print("hello!");
    }}}

测试类

1
2
3
Mythread m=new Mythread();
new Thread(m).start();
new Thread(m).start();

这里主要向Runnable接口实现的实例作为参数传递给Thread,再启动线程,这里就有一个问题,因为不同线程操作一个对象,所以会造成线程不安全的问题,也就是脏数据

线程的暂时停止

利用Thread的sleep方法可暂时停止线程当前的操作,如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Mythread implements Runnable
{
@Override
public void run() {
for(int i=0;i<5;i++){
System.out.print("hello!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}}}

这里的参数是ms,所以1000就是1s了

线程的共享互斥

在程序里,多个线程可自由操作,就会造成一个线程还在执行,另一个线程就插进来执行,就会造成一些脏数据的产生
如:

1
2
3
4
5
6
7
8
9
private int count=1000;
@Override
public void run() {
for(int i=0;i<100;i++){
count--;
System.out.println("mythread"+(count));
}
}

测试类:

1
2
3
4
Mythread m=new Mythread();
new Thread(m).start();
new Thread(m).start();
new Thread(m).start();

测试结果:(取部分)

mythread999
mythread997
mythread996
mythread995
mythread994

显然出现了脏数据
要想使数据正确,一种方法是加同步锁,即一个线程在执行该操作时,其余线程无锁,则必须等待,直到该线程执行结束或者被挂起,才释放锁,然后在门外等待的线程依次执行,加锁,重复上述步骤
显然,就不会出现多个线程操作同一个对象了

java中通过声明synchronized关键字实现同步锁
如:

public synchronized void run()

则在执行run方法前加同步锁,控制线程,当然该关键字可以声明在任何方法前,包括静态方法
另外,它还有另外一种声明方式

public void run() {
synchronized (xx.class) {
}

注意这里xx.class是操作该方法的那个类实现同步比如这里可以将

synchronized (this) {}

就是对本身这个类加锁控制了等同于上述的另一种写法

线程的协调

如果有一个线程正在执行同步锁,而其他线程则要等待,这是典型的线程互斥现象
假如当该空间有空余时则写入数据,无空闲时间则等待(等待)
当该空间有空闲时,则通知等待的线程(通知)

这里是根据空间是否空闲为条件进行线程处理,java中通过wait(让线程等待),notify与notifyAll(启动等候中的线程)

我们可以把线程处于等待的地方形象的看做wait set(线程的休息室)

wait-把线程放入休息室

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static int count=90;
public synchronized int s() throws InterruptedException{
count--;
System.out.println("ok:"+count);
this.wait();
System.out.println("lockover");
return count;
}
@Override
public void run() {
try {
s();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

然后在测试中启动几个线程

1
2
3
4
Count m=new Count();
new Thread(m).start();
new Thread(m).start();
new Thread(m).start();

测试结果:

ok:89
ok:88
ok:87

一般为obj.wait(),而obj是某个对象的实例,也就是说该线程在obj的休息区
之后会自动释放锁,等待的线程开始执行

当然也有obj.wait(1000);也可以设置在休息区等待的秒数,然后继续执行

测试结果:

ok:89
ok:88
ok:87
lockover
lockover
lockover

notify方法-从休息区拿出线程

一般为obj.notify(),即从obj的wait set里的线程中挑选一个,唤醒一个线程并退出该休息区
当然,这里有一个顺序,召唤该线程的那个线程执行完,释放锁后,那个在休息区的线程在执行

obj.notifyAll(),即从线程区中拿出所有线程

最后需要说一下的是wait、notify、notifyAll是Object的类方法,也就是所有类都能使用

由此可看出多线程的确是一个复杂而且很重要的东西!!

热评文章