Java多线程

前言:线程是java的重要功能之一,多线程能使程序反应更快,交互性更强,执行效率更高。

一、任务和线程的创建

任务就是对象。为任务创建线程可以简单分为以下两种:1.继承Thread类 2.实现Runnable接口。
示例程序片段:
1.继承Thread类

class Task extends Thread{
    @Override
    public void run() {
        super.run();
    }
}

public class Main {
    public static void main(String[] args) {
        Task task = new Task(); //创建任务
        task.start();    //告诉java虚拟机该线程准备运行,然后java虚拟机通过调用run()方法执行任务
    }
}

2.实现Runnable接口

class Task implements Runnable{
    @Override
    public void run() {
    }
}
public class Main {
    public static void main(String[] args) {
        Task task = new Task(); //创建任务
        Thread thread = new Thread(task);
        thread.start();    //告诉java虚拟机该线程准备运行,然后java虚拟机通过调用run()方法执行任务
    }
}

推荐使用方法2,原因如下:
1.由于java不能实现多继承而可以实现多个接口,如果一个类继承了其他类,就导致无法继续继承Thread类。
2.使用1方法会是任务和运行任务混在一起。

附:
1.Thread类
Thread类除了上述为任务创建线程之外,还有许多控制线程的方法。Thread类同时也实现了Runnable接口。

+isAlive() : boolean  //测试当前任务是否正在运行
+setPriority(p :int) : void  //设置线程优先级(1-10)   
+join() : void  //使当前等待线程结束
+sleep(ms : long) //线程休眠时长
+yield() : void  //使线程暂停并允许执行其他线程
+interrupt() : void //中断线程

2.线程状态

该图片转自http://blog.csdn.net/huang_xw/article/details/7316354

二、线程池

当一个程序有大量任务且需要为每个任务都创建一个新线程时,创建线程池会是个好方法。以下为两个重要接口。
Executor接口:执行线程池中的任务。
ExecutorService接口:管理和控制任务,是Executor的子接口。
有以下主要方法:

+execute(Runnable r) : void  //执行任务
+shutdown() : void //关闭执行器,不再接收新任务,但会完成当前未完成的任务。
+shutdownNow() : List<Runable> //关闭执行器,不再接收新任务,当前未完成的任务停止工作并返回未完成的任务表。
+isShutdown() : boolean //执行器已关闭返回ture
+isTerminated() : boolean //如果线程池中的任务都终止,返true

为了创建执行器,即Executor对象,需要使用Executors类中的静态方法

+newFixedThreadPool(numOfThread : int) : ExecutorService //创建一个固定线程数目的线程池,如果任务完成,则会重新使用以执行另一线程。
+newCachedThreadPool() : ExecutorService //为每个等待的任务创建线程。

线程池示例:

public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3); //创建固定线程数目的线程池
        //ExecutorService executor = Executors.newCachedThreadPool();//该方法按需创建线程数目
        executor.execute(new Task()); //执行任务
        executor.execute(new Task());
        executor.execute(new Task());
        executor.shutdown(); //关闭执行器,不再接收任务
    }
}

三、线程同步

当多个线程同时访问同一资源时,可能会导致数据破坏。如下示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*此程序为该类的num添加数值,创建100个线程,每个线程为num加1*/
class Number { 
    private int num = 0;

    public void addNum(int n) {
        try {
            int newNum = num + n;
            Thread.sleep(5);
            num = newNum;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } 
    }

    public int getNum() {
        return num;
    }
}

public class Main {
    public static Number number = new Number();

    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 1; i <= 100; i++) {
            executor.execute(new AddNum()); //创建100个线程,每个线程为num加1
        }
        executor.shutdown();

        while (!executor.isTerminated()) { 
        }
        //任务完成
        System.out.println(number.getNum());
    }


    static class AddNum implements Runnable{
        @Override
        public void run() {
            number.addNum(1);

        }
    }
}

该程序输出的最终值不确定(或2或3或其他),并非100。该程序因为多个线程访问同一资源而使数据破坏。当线程1执行到int newNum = num + n但还未执行num = newNum时,线程2可能执行到nt newNum = num + n,导致此处的num仍然是之前的num,并没有发生改变,从而导致数据发生错误。这中问题成为竞争状态,存在线程安全问题。
解决方案:线程同步
1.使用synchronized关键字(隐式加锁)

public synchronized void addNum(int n) { //被同步的代码块称为同步块
    //...
}

或者

public void addNum(int n) {
        synchronized(this) {
            try {
                int newNum = num + n;
                Thread.sleep(5);
                num = newNum;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } 
    }

当线程1进入同步块时,会对number对象进行加锁,线程执行完成后释放锁。当线程1的number对象加锁时,其他线程的对象就只能等待线程1释放锁才能对number对象加锁。通过此种方式有效地避免了竞争状态,实现了线程安全。

2.利用加锁同步(显式加锁)
Lock接口:定义了可以加锁和释放锁的方法。

+lock() : void //加锁
+unlock() : void //释放锁

可以通过创建ReentrantLock类的对象来创建锁。

+ReentrantLock() //与ReentrantLock(false)等价
+ReentrantLock(b : boolean) //创建一个具有公平策略的锁,true则等待时间最长的任务获得锁,false则线程执行没有特定顺序

加锁示例:

class Number { 
    private int num = 0;
    public void addNum(int n) {
        lock.lock();    //加锁
        try {
            int newNum = num + n;
            Thread.sleep(5);
            num = newNum;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); //释放锁
        }   
    }
    public int getNum() {
        return num;
    }
}

附:线程同步之单例模式
在程序设计中,往往一个类只能有一个对象,会采用单例模式。示例如下:

public class Model {
    private static Model model = null;
    private Model() { //外界不能实例化
    }

    public static Model getInstance() { //通过此获得对象,只能有一个对象
        if (model == null) {
            model = new Model();
        } 
        return model;
    }
}

在多线程中,如果每个线程都要对Model的对象进行操作,就会出现数据错误。假设线程1执行到if (model == null)但却未执行到model = new Model()时线程2也进入该方法,此时model对象仍然为空,会进入if的语句里,当执行完毕后就产生了两个不同model对象,导致线程安全问题。因此,在设计单例模式时同时要考虑线程安全问题。可以通过同步加锁来解决。例如:

public static synchronized Model getInstance(){
    //...
}

但是在方法头使用synchronized来锁住对象会明显占用资源,因为每当程序调用该方法时都需要给对象加锁,可以如下改进:

public static Model getInstance() {
        if (model == null) {
            synchronized(model) {
                if (model == null)
                    model = new Model();
            }
        } 
        return model;
    }

五、线程死锁

当两个或多个线程需要在几个共享资源上获得锁时,就可能产生死锁。如下示例:

/*线程1*/
synchronized (object o1) {
    //...
    synchronized (object o2){ //等待线程2释放object2的锁
    //...
    }
}
/*线程2*/
synchronized (object o2) {
    //...
    synchronized (object o1){ //等待线程1释放object1的锁
    //...
    }
}

上述情况会导致死锁,两个程序都无法继续运行。因此在设计程序中应确保每个线程都能获得对应的锁,避免死锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值