您的位置:首页 > 编程语言 > Java开发

Java多线程

2015-06-15 20:32 507 查看

Java多线程

一、多线程的概念

我们要想了解线程就必须先了解进程,进程通俗点讲就是正在运行的程序,而线程就是进程中的一个负责程序执行的控制单元。一个进程可以有多个执行路径,称之为多线程,一个进程中至少有一个线程,开启多线程是为了同时运行多部分代码,每个线程都有自己的运行内容,这个内容可以称为线程要执行的任务。

二、创建线程

(1)创建线程的方法之一
创建新执行的线程有两种方法其中一种方法是将类声明为Thread的子类,该子类应该重写Thread类的run方法,直接创建创建Thread的子类对象创建线程,调用start方法开启线程并调用线程的任务run方法执行。
使用此方法调用run方法和调用的线程的start方法有什么不同请看下面的代码
<span style="font-size:14px;"><span style="font-size:14px;">public class ThreadTest {

public static void main(String[] args) {
Thread th1 = new ThreadDemo("线程1号");
Thread th2 = new ThreadDemo("线程2号");
th1.run();
th2.run();
}

}

class ThreadDemo extends Thread{

private String name;
public ThreadDemo(String name) {
this.name  = name;
}
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
for(int i = 0;i<5;i++){
System.out.println("线程的名称"+name);
}
}
}</span></span>
运行截图



调用start方法
<span style="font-size:14px;"><span style="font-size:14px;">public class ThreadTest {

public static void main(String[] args) {
Thread th1 = new ThreadDemo("线程1号");
Thread th2 = new ThreadDemo("线程2号");
th1.start();
th2.start();
}
}public class ThreadTest {

public static void main(String[] args) {
Thread th1 = new ThreadDemo("线程1号");
Thread th2 = new ThreadDemo("线程2号");
th1.start();
th2.start();
}
}</span></span>
运行效果



如果出不来上面的效果那么在run方法中加入下面的语句
<span style="font-size:14px;"><span style="font-size:14px;">@Override
public void run() {
// TODO Auto-generated method stub
super.run();
for(int i = 0;i<5;i++){
for(int j = -999;j<999;j++){
}
System.out.println("线程的名称"+name);
}</span></span>
<span style="font-size:14px;"><span style="font-size:14px;">}</span></span>
如果是使用run方法那么和普通的类执行情况差不多都在主线程中顺序执行,如果是调用start方法则是启动线程让其自己抢占资源。使用start方法启动线程之后有三个线程在抢占资源th1,th2,和主线程。
(2)线程的名称
可以通过Thread的getName()方法来获取线程的名称下面我们首先将run方法中的输出语句改为System.out.println(getName())并且在测试类中执行线程中的run方法,运行后出现下面的效果图



我们之前说过如果直接调用run方法的话就相当于是普通类那样运行,那么为什么打印出来的线程名称还是两个不同的呢,主要是因为在main 方法中我们创建线程的时候就系统就设定好了线程的名字,默认是按照Thread-阿拉比数字增加的方式定义的不过如果我们当将run方法中的输出语句改为下面System.our.println(currentThread.getName())那么就会出现打印出来的全部是main。
既然这样那么我们可不可以自己定义线程的名字呢,可以就是在线程的实现类的构造函数的第一句话中使用super(name)这里
的name就是线程的名字。



(3)线程的四种状态



(4)创建线程的第二种方式
创建线程的第二种方式就是声明实现Runnable接口的类,该类然后实现run方法。需要注意的是通过Thread类来创建线程对象并将Runable接口的子类对象作为构造函数的参数进行传递,最后调用线程的start方法开启线程。
那么既然创建线程有两种方式那么使用哪种比较好呢?答案是第二种,那么使用第二种方式有什么好处呢?实现Runnable接口的方式创建线程的好处就是将线程的任务从线程的子类中分离出来了,进行了单独的封装,按照面向对象的思想将任务封装成看对象,还有就是避免了Java单继承的局限性。
(5)使用多线程的方式完成卖票。
现在有一个需求就是使用多线程模拟四个窗口同时卖150张票,需要搞清楚的是4个窗口一共卖150张票不是每个窗口都卖150张。
<span style="font-size:14px;"><span style="font-size:14px;">class Ticket  implements Runnable {

private int num;                     //票的总数
public Ticket(int num){              //票的总数通过构造方法传入
this.num = num;
}
@Override
public void run() {
sale();
}

public void sale(){
//只有当余票的数量大于0的时候才执行卖票的操作
while(num>0){
//打印出当前是哪个线程卖出了第几张票
System.out.println(Thread.currentThread().getName()+"买出第"+num--+"票");
}
}
}

public class TicketTest{
public static void main(String[] args) {
//只创建一个任务
Ticket t = new Ticket(150);
//使用四个线程模拟4个卖票的窗口,执行同一个任务。
Thread th1 = new Thread(t);
Thread th2 = new Thread(t);
Thread th3 = new Thread(t);
Thread th4 = new Thread(t);
th1.start();
th2.start();
th3.start();
th4.start();
}
}</span></span>
运行效果截图



(6)线程的安全问题
上面卖票的例子如果在run方法中执行了Thread.sleep(1000);的操作的话就会发生卖出错号票的问题,这就出现了线程的安全问题,那么线程的安全问题是由什么造成的呢?
主要是因为多线程在操作共享的数据,操作共享数据的线程代码有多条。具体到上面的卖票的例子就是在while循环中判断num的时候比如在还有1张票的时候0号线程和1号线程同时抢占资源0号线程抢占到卖了1张票变成了0张票但是1号线程马上又卖票造成卖出票数是-1的问题。主要就是多线程同时并发操作num.
针对线程的安全问题我们可以使用同步代码块的方式来解决,就是使用Java的关键字synchronized来锁住一个对象下面是改造后的代码。
<span style="font-size:14px;"><span style="font-size:14px;">public void sale() {
// 只有当余票的数量大于0的时候才执行卖票的操作
while (num > 0) {
synchronized (this) {
if (num > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 打印出当前是哪个线程卖出了第几张票
System.out.println(Thread.currentThread().getName() + "买出第"
+ num-- + "票");
}
}
}
}</span></span>

<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-family: Arial, Helvetica, sans-serif;"><span style="font-size:14px;">同步的前提就是多线程使用同一把锁</span></span></span></span>

还有一种解决的方式就是使用同步的函数,同步函数的特点是在要被同步的方法的前加synchronized关键字,这样做我们一定需要注意就是什么需要同步什么不需要同步,下面我们将上面的卖票的例子给改造成同步函数的形式。
<span style="font-size:14px;"><span style="font-size:14px;">@Override
public void run() {
// 只有当余票的数量大于0的时候才执行卖票的操作
while (num > 0) {
sale();
}
}

public synchronized void sale() {
if (num > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 打印出当前是哪个线程卖出了第几张票
System.out.println(Thread.currentThread().getName() + "买出第" + num--+ "票");
}
}</span></span>
需要注意的是静态的同步函数使用的锁是该函数所属的字节码文件对象,可以使用getClass()来获取也可以使用类名.class来获取。
不知道大家注意到没单例模式的延迟加载模式也存在着线程的安全问题就是,当判断instance实例是不是为空的时候可能会由多线程操作引发安全问题,那我们就让获取实例的函数变为同步函数,这样做虽然可以解决问题但是造成了效率低下问题。那么我们做下面的改造。
<span style="font-size:14px;"><span style="font-size:14px;">public class SingleInstance {

private static SingleInstance instance;
private SingleInstance(){}

public static SingleInstance getInstance(){
if (instance == null) {
synchronized (SingleInstance.class) {
instance = new SingleInstance();
}
}
return null;
}
}</span></span>
(7)线程间的通信
现在我们想象这样的一个需求就是使用一个线程进行数据的存储使用另外的一个线程进行数据的读取,并且在内容为空的时候不读取,在有输入内容的时候才进行读取。
<span style="font-size:14px;"><span style="font-size:14px;">public class ThreadCommunication {

public static void main(String[] args) {

Resource r = new Resource();                 //被操作的资源是唯一的
Thread th1 = new Thread(new Input(r));
Thread th2 = new Thread(new Output(r));
th1.start();
th2.start();
}
}

class Input implements Runnable{

private boolean isRun = true;       //线程的标志位,用来控制线程的运行的
private Resource r;
Input(Resource r){
this.r = r;
}
@Override
public void run() {
int x = 0;
int count = 0;
while(isRun){
if (x == 0) {
r.set("小明", 23);
}else{
r.set("小华", 18);
}
count = count + 1;
x = (x+1)%2;
if(count == 10){
isRun = false;
}
}
}
}

class Output implements Runnable{

private Resource r;

public Output(Resource r){
this.r = r;
}

@Override
public void run() {
while(true){
r.out();
}
}
}

class Resource {

private String  name;
private Integer age;

private boolean isEmpty = true;          //判断是否为空的

public synchronized void set(String name,Integer age){
if(!isEmpty)
try {
this.wait();             //如果不为空的话就让输入线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = name;
this.age = age;
isEmpty = false;
this.notify();

}

public synchronized void out(){
if (isEmpty)
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("姓名是"+this.name+"--年龄是"+age);
isEmpty = true;
this.notify();
}
}</span></span>
上面我们使用了两个标志位一个就是数据资源的标志位根据这个标志位来进行线程间的通信,还有一个就是线程的标志位用来控制线程的执行周期的,还有在Resource类中的两个同步方法这必须定义在同步中。因为这些方法是用于操作线程状态的方法。必须要明确到底操作的是哪个锁上的线程。
在线程的通信中涉及到的方法是wait(),让线程处于冻结状态,被wait的线程会被存储到线程池中。notify()唤醒线程池中一个线程(任意)notifyAll()唤醒线程池中的所有线程。这三个方法全部被定义在了Object类中了这是为什么呢?因为这些方法是监视器的方法。监视器其实就是锁。锁可以是任意的对象,任意的对象调用的方式一定定义在Object类中。
在上面的例子中我们可以看到就是我们只启动另一个输入线程和一个输出线程,如果我们有多个输入线程和多个的输出线程结果会怎么样呢?下面我们就以启动2个输入线程t0,t1和两个输出线程t2,t3为例,如果刚开始4个线程中t0抢到执行权,输入了数据,此时的唤醒操作是没有意义的因为线程池中没有因为wait而等待的线程,此时t0还有执行权继续运行被要求等待,所以t1,t2,t3中的一个任意一个抢到执行权假如说是t1,t1等待,比如现在t2抢到执行权输出了数据,唤醒现在线程池中的wait线程有t0和t1比如唤醒了t0输如了一条数据,继续唤醒t1,t1没有判断直接又输入了数据,这时就出现了安全问题就是接连输入了两次数据。
造成这个问题的原因就是没有进t1被唤醒时没有没有继续判断,那么我就进行改造就是直接将input和output的判断由if改为while那么这样就好了吗还是按照上面的t0开始运行后等待,t1等待,t2输出,t2等待,随机唤醒了t3,t3也等待这就造成了死锁问题,这里又是唤醒出了问题,每次只是随机唤醒一个,如果我们每次唤醒对方没问题如果唤醒我方就有问题了,所以我们的解决办法就是直接唤醒全部的,将notify改为notifyAll(),这是在jdk1.5以前的处理办法在jdk1.5开始出现了新的解决办法。
在JDK1.5版本后提供了接口Lock类,Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的Condition对象。synchronized 方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。
Lock接口: 出现替代了同步代码块或者同步函数。将同步的隐式锁操作变成现实锁操作。同时更为灵活。可以一个锁上加上多组监视器。lock():获取锁。unlock():释放锁,通常需要定义finally代码块中
Condition接口:出现替代了Object中的wait notify notifyAll方法。将这些监视器方法单独进行了封装,变成Condition监视器对象。

可以任意锁进行组合。
改造后的代码如下
<span style="font-size:14px;">public class ThreadCommunication {

public static void main(String[] args) {

Resource r = new Resource();                 //被操作的资源是唯一的
Thread th1 = new Thread(new Input(r));
Thread th2 = new Thread(new Input(r));
Thread th3 = new Thread(new Output(r));
Thread th4 = new Thread(new Output(r));
th1.start();
th2.start();
th3.start();
th4.start();
}
}

class Input implements Runnable{

private boolean isRun = true;       //线程的标志位,用来控制线程的运行的
private Resource r;
Input(Resource r){
this.r = r;
}
@Override
public void run() {
int x = 0;
int count = 0;
while(isRun){
if (x == 0) {
r.set("小明", 23);
}else{
r.set("小华", 18);
}
count = count + 1;
x = (x+1)%2;
if(count == 10){
isRun = false;
}
}
}
}

class Output implements Runnable{

private Resource r;

public Output(Resource r){
this.r = r;
}

@Override
public void run() {
while(true){
r.out();
}
}
}

class Resource {

private String  name;
private Integer age;

private Lock lock = new ReentrantLock();
Condition inputLock = lock.newCondition();
Condition outputLock = lock.newCondition();
private boolean isEmpty = true;          //判断是否为空的

public void set(String name,Integer age){
lock.lock();
try{
while(!isEmpty)
try {
inputLock.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.name = name;
this.age = age;
System.out.println("我是生产者我存入数据"+name +"--"+age);
isEmpty = false;
outputLock.signal();
}finally{
lock.unlock();
}

}

public synchronized void out(){
lock.lock();
try{
while (isEmpty)
try {
outputLock.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("姓名是"+this.name+"--年龄是"+age);
isEmpty = true;
inputLock.signal();
}finally{
lock.unlock();
}
}
}</span>
(8)wait 和 sleep 区别?

wait可以指定时间也可以不指定。sleep必须指定时间。

在同步中时,对cpu的执行权和锁的处理不同。wait:释放执行权,释放锁。sleep:释放执行权,不释放锁。

(9)停止线程的操作
一般我们停止线程使用的是定义标记的方式,上面我们在set中也用到了,但是如果线程的循环里面是wait()的话就会读不到标记,导致即便设置了标记也不会停止线程。
如果线程处于了冻结状态,无法读取标记。如何结束呢?可以使用interrupt()方法将线程从冻结状态强制恢复到运行状态中来,让线程具备cpu的执行资格。 当时强制动作会发生了InterruptedException,记得要处理

(10)守护线程
线程里面有一个设置一个线程为守护线程,守护线程其实就是一个后台线程,就是当前台线程全部结束的时候,后台线程自动结束,那么这个在哪里用到呢就是比如上面的一个线程进行输入数据,一个线程进行输出数据那么话如果输入线程结束的话那么输出线程也应该直接结束了。特别注意的就是设置一个线程是守护线程的话必须在其启动前进行设置。
(11)线程中的其他方法
1.join方法就是申请运行加入运行,如果现在程序中有主线程和t1,t2线程如果启动t1后调用此方法则主线程会等t1执行完再执行
2.toString()方法,线程的toString方法会打印出线程的名字,优先级,线程组,线程的优先级有1-10这么10个优先级越高只能说明其抢到CPU的执行权的几率比较大不是绝对。
3.yield方法就是让当前线程暂停一下不会一直占着CPU的执行权
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: