Java-线程安全和线程池

一、多线程的数据安全问题

1、经典案例:多个窗口买票的问题。

public class Tickets {

    public static void main(String[] args) {
        // int num = 0;
        TicketsThread task = new TicketsThread();

        Thread window1 = new Thread(task, "001");
        Thread window2 = new Thread(task, "002");
        Thread window3 = new Thread(task, "003");
        Thread window4 = new Thread(task, "004");
        window1.start();
        window2.start();
        window3.start();
        window4.start();
    }
}

class TicketsThread implements Runnable {
    int tickets = 10;
    @Override
    public void run() {
        while (tickets > 0) {
            System.out.println("窗口"+Thread.currentThread().getName() + "正在出售第"
                    + tickets + "张票");
            tickets--;
        }
    }
}

输出结果:

窗口001正在出售第10张票
窗口001正在出售第9张票
窗口001正在出售第8张票
窗口001正在出售第7张票
窗口002正在出售第10张票//重复的票
窗口002正在出售第5张票
窗口002正在出售第4张票
窗口002正在出售第3张票
窗口002正在出售第2张票
窗口002正在出售第1张票
窗口001正在出售第6张票
窗口003正在出售第4张票//重复的票
窗口004正在出售第5张票//重复的票

在一个线程已经访问了车票的资源,但是还没有执行到把票数减去1的操作前,有其他的线程对车票的资源进行了访问,此时车票数量还未变化,但是实际上车票数量应该减去1了,造成了数据不一致,所以到这两个线程都执行完后,就出现了出售同一张票的情况。
从CPU执行的角度理解就是,当CPU在执行一个线程,当前的这个线程执行到某一行代码的同时,又来了一个线程,这两个线程都有执行资格,具体执行哪一个线程是由CPU决定的。当后来的线程得到了执行资格后,前一个线程就处于阻塞状态并保存当前线程的状态,无法继续执行,当重新获得执行资格后,此时有关的变量值可能已经变化了,但是这个线程是不知道的,继续按照保存的状态继续执行代码,出现了数据不一致,最终出售了相同的票。

2、线程同步

要保证最终的车票不会出现重复的情况,需要保证线程在执行买票的这个动作的时候,要完整的执行完这个动作,如果当前线程没有执行完,其他线程需要等待,这就是线程同步。
线程同步的原理是锁机制,就像 对某一块代码“上锁”,只有当前线程有这把锁的“钥匙”,只有它能够获得这段代码,当执行完后,对这块代码进行“开锁”,其他的线程就也可以访问这段代码了。
线程同步的方法:
a)同步代码块
b)同步方法
c)对象互斥锁
d)单例懒汉式的线程
e)JDK5的新特性Lock的使用

在不同的位置对代码进行加锁,也会有不同的效果:
例1:

public class Synchronizeds {

    public static void main(String[] args) {
        TicketsThread task = new TicketsThread();
        new Thread(task, "001").start();
        new Thread(task, "002").start();
        new Thread(task, "003").start();
        new Thread(task, "004").start();
    }
}

class TicketsThread implements Runnable {
    private int tickets = 10;
    private Object o = new Object();

    @Override
    public void run() {
        int count = 0;
        // 同步代码块
         syn1();
    }

private void syn1() {
        while (tickets > 0) {
            synchronized (o) {
                System.out.println("窗口" +Thread.currentThread().getName()+ "正在出售第" + tickets + "张票");
                tickets--;
                try {
                    Thread.sleep(10);//为了更清楚结果,对线程进行休眠
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

输出结果:

窗口001正在出售第10张票
窗口004正在出售第9张票
窗口004正在出售第8张票
窗口004正在出售第7张票
窗口004正在出售第6张票
窗口004正在出售第5张票
窗口004正在出售第4张票
窗口004正在出售第3张票
窗口004正在出售第2张票
窗口004正在出售第1张票
窗口003正在出售第0张票
窗口002正在出售第-1张票
窗口001正在出售第-2张票

结果中没有重复的票了,但是出现了第1张票以外的票。
例2:把同步锁放在循环外

private void syn2() {
        synchronized (o) {
            while (tickets > 0) {
                System.out.println("窗口" +Thread.currentThread().getName()+ "正在出售第" + tickets + "张票");
                tickets--;
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }

输出结果:只有一个窗口在卖票,其他线程没有执行到。

窗口001正在出售第10张票
窗口001正在出售第9张票
窗口001正在出售第8张票
窗口001正在出售第7张票
窗口001正在出售第6张票
窗口001正在出售第5张票
窗口001正在出售第4张票
窗口001正在出售第3张票
窗口001正在出售第2张票
窗口001正在出售第1张票

例3:在出售每一张票后判断是否还有票。

private void syn3() {
        while (true) {
            synchronized (o) {
                if (tickets > 0) {
                    System.out.println("窗口" +Thread.currentThread().getName()+ "正在出售第" + tickets + "张票");
                    tickets--;
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }

输出结果:正常

窗口001正在出售第10张票
窗口001正在出售第9张票
窗口001正在出售第8张票
窗口001正在出售第7张票
窗口001正在出售第6张票
窗口003正在出售第5张票
窗口003正在出售第4张票
窗口003正在出售第3张票
窗口003正在出售第2张票
窗口003正在出售第1张票

例4:同步方法

private void syn4() {
        while (true) {
            // 同步方法
            if (synmethod()) {
                break;
            }
        }
    }

private boolean isOK;
public synchronized boolean synmethod() {
        if (tickets > 0) {
            System.out.println("窗口" +Thread.currentThread().getName()+ "正在出售第" + tickets + "张票");
            tickets--;
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            isOK = true;
        }
        return isOK;
    }

输出结果:正常

窗口001正在出售第10张票
窗口004正在出售第9张票
窗口003正在出售第8张票
窗口003正在出售第7张票
窗口003正在出售第6张票
窗口003正在出售第5张票
窗口003正在出售第4张票
窗口003正在出售第3张票
窗口002正在出售第2张票
窗口002正在出售第1张票

例5:Lock方法

private Lock l = new ReentrantLock();
private void syn5() {
        while (true) {
            l.lock();// 实现Lock接口
            try {
                if (tickets > 0) {
                    System.out.println("窗口" +Thread.currentThread().getName()+ "正在出售第" + tickets + "张票");
//                  count++;
                    tickets--;
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            } finally {
                l.unlock();
            }
        }
    }

输出结果:正常

窗口001正在出售第10张票
窗口001正在出售第9张票
窗口001正在出售第8张票
窗口003正在出售第7张票
窗口003正在出售第6张票
窗口003正在出售第5张票
窗口003正在出售第4张票
窗口002正在出售第3张票
窗口004正在出售第2张票
窗口004正在出售第1张票

3、死锁

a) 根源:如果出现同步嵌套(同步里面包含同步),就有可能会产生死锁问题
b) 最常见的死锁形式是当线程1 持有对象A 上的锁,而且正在等待对象B 上的锁;而线程2 持有对象B 上的锁,却正在等待对象A 上的锁。这两个线程永远都不会获得第二个锁,或是释放第一个锁,所以它们只会永远等待下去。—–死锁(只有对方都拥有了彼此的资源且不愿意释放时,才会出现死锁问题)
例子:模拟死锁
i. 创建一个类,里面创建两个静态常量对象作为两把锁

public class MyLock {
    public static String lock1=new String();
    public static String lock2=new String();
}

ii. 在一个线程类中,同步嵌套,且使用这两把锁

public class DeadLockThread extends Thread {
    private boolean flag;
    public DeadLockThread(String name,boolean flag){
        super(name);
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            synchronized (MyLock.lock1) {
                System.out.println(Thread.currentThread().getName() + ",获得a这把锁");
                System.out.println(Thread.currentThread().getName()+ ",准备获取b这把锁");
                synchronized (MyLock.lock2) {
                    System.out.println(Thread.currentThread().getName()+ ",获得b这把锁");
                    System.out.println(Thread.currentThread().getName()+ ",成功获得宝藏");
                }
            }
        } else {
            synchronized (MyLock.lock2) {
                System.out.println(Thread.currentThread().getName() + ",获得b这把锁");
                System.out.println(Thread.currentThread().getName()+ ",准备获取a这把锁");
                synchronized (MyLock.lock1) {
                    System.out.println(Thread.currentThread().getName()+ ",获得a这把锁");
                    System.out.println(Thread.currentThread().getName()+ ",成功获得宝藏");
                }
            }
        }
    }
}

iii. 创建两个不同的线程,走不同的路线,造成互相等待的现象

public static void main(String[] args) {
    DeadLockThread zhangsan = new DeadLockThread("zhangsan", true);
    DeadLockThread lisi = new DeadLockThread("lisi", false);
    zhangsan.start();
    lisi.start();
}  

结果:两个人都在等待对方的那把锁,无法获得宝藏。

lisi,获得b这把锁
lisi,准备获取a这把锁
zhangsan,获得a这把锁
zhangsan,准备获取b这把锁

要解决死锁的问题,可以调用wait(),notify(),notifyAll()方法
例:创建生产者消费者模型,每生产一个产品,就消费一个产品。

public class Test {
    public static void main(String[] args) {
        ShareDate sd = new ShareDate();
        Pro p = new Pro(sd);
        Custor c = new Custor(sd);
        p.start();
        c.start();
    }
}

//生产者类
class Pro extends Thread{
    private ShareDate sd;
    public Pro(ShareDate sd){
        this.sd = sd;
    }
    @Override
    public void run() {
        for (char c = 'a';c<='d';c++) {
            try {
                Thread.sleep((int)(Math.random()*3000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            sd.pushDate(c);
        }
    }
}


//消费者类
class Custor extends Thread{
    private ShareDate sd;
    public Custor(ShareDate sd){
        this.sd = sd;
    }
    @Override
    public void run() {
        char c ;
        do{
            try {
                Thread.sleep((int)(Math.random()*3000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            c=sd.getData();
        }while(c!='d');
    }
}



//资源类
class ShareDate{
    private char c; //有字符
    //生产者与消费者互相通知的一个标识
    private boolean flag = false; //表示没有字符,消费者不能消费

    //生产的方法
    public synchronized void pushDate(char c){
        //表示可以生产
        if(flag){
            System.out.println("消费者还没有消费,因此生产者还不能生产");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.c = c; //生产字符
        flag = true; //表示通过消费者来消费
        this.notify();
        System.out.println("生产者已经生产完字符:"+c+"请消费者来消费");
    }

    //拿字符的方法
    public synchronized char getData(){
        if(!flag){
            System.out.println("生产者还没有生产,因此消费者还不能消费");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notify();
        System.out.println("消费者已经拿到字符:"+c+"请生产者来生产");
        return c;
    }
}

输出结果:

生产者还没有生产,因此消费者还不能消费
生产者已经生产完字符:a请消费者来消费
消费者已经拿到字符:a请生产者来生产
消费者已经拿到字符:a请生产者来生产
消费者还没有消费,因此生产者还不能生产
消费者已经拿到字符:a请生产者来生产
生产者已经生产完字符:b请消费者来消费
消费者已经拿到字符:b请生产者来生产
消费者还没有消费,因此生产者还不能生产
消费者已经拿到字符:b请生产者来生产
生产者已经生产完字符:c请消费者来消费
消费者还没有消费,因此生产者还不能生产
消费者已经拿到字符:c请生产者来生产
生产者已经生产完字符:d请消费者来消费
消费者已经拿到字符:d请生产者来生产

4、ThreadLocal类

ThreadLocal类
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

protected T initialValue()

返回此线程局部变量的当前线程的“初始值”。线程第一次使用 get() 方法访问变量时将调用此方法,但如果线程之前调用了 set(T) 方法,则不会对该线程再调用 initialValue 方法。通常,此方法对每个线程最多调用一次,但如果在调用 get() 后又调用了 remove(),则可能再次调用此方法。
该实现返回 null;如果程序员希望线程局部变量具有 null 以外的值,则必须为 ThreadLocal 创建子类,并重写此方法。通常将使用匿名内部类完成此操作。

public T get()

返回此线程局部变量的当前线程副本中的值。如果变量没有用于当前线程的值,则先将其初始化为调用 initialValue() 方法返回的值。

public void set(T value)

将此线程局部变量的当前线程副本中的值设置为指定值。大部分子类不需要重写此方法,它们只依靠 initialValue() 方法来设置线程局部变量的值。

public void remove()

移除此线程局部变量当前线程的值。如果此线程局部变量随后被当前线程读取,且这期间当前线程没有设置其值,则将调用其 initialValue() 方法重新初始化其值。这将导致在当前线程多次调用 initialValue 方法

例:ThreadLocal的使用

public class ThreadLocalTest {
    public static void main(String[] args) {
        Num num = new Num();
        TestThread test = new TestThread(num);
        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(test);
            thread.start();
        }

    }

    static class Num {
        private ThreadLocal tl = new ThreadLocal() {
            protected Integer initialValue() {
                return 0;
            };
        };

        public int getNum() {
            // TODO Auto-generated method stub
            tl.set(tl.get() + 1);
            return tl.get();
        }
    }

    static class TestThread implements Runnable {
        private Num num;

        public TestThread(Num num) {
            super();
            this.num = num;
        }

        @Override
        public void run() {
            // TODO Auto-generated method stub
            for (int i = 0; i < 2; i++) {
                System.out.println(Thread.currentThread().getName() + ":"
                        + num.getNum());
            }

        }

    }

}

输出结果:

Thread-0:1
Thread-0:2
Thread-1:1
Thread-1:2
Thread-2:1
Thread-2:2

虽然是操作了同一个对象,但是每个每个线程都是独立的,每个线程中的数字都是从1开始

ThreadLocal和其它所有的同步机制都是为了解决多线程中的对同一变量的访问冲突,在普通的同步机制中,是通过对象加锁来实现多个线程对同一变量的安全访问的。这时该变量是多个线程共享的,使用这种同步机制需要很细致地分析在什么时候对变量 进行读写,什么时候需要锁定某个对象,什么时候释放该对象的锁等等很多。
所有这些都是因为多个线程共享了资源造成的。ThreadLocal就从另一个角 度来解决多线程的并发访问,ThreadLocal会为每一个线程维护一个和该线程绑定的变量的副本,从而隔离了多个线程的数据,每一个线程都拥有自己的 变量副本,从而也就没有必要对该变量进行同步了。
ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的整个变量封装进 ThreadLocal,或者把该对象的特定于线程的状态封装进ThreadLocal。
当然ThreadLocal并不能替代同步机制,两者面向的问题领域不同。
同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信 的有效方式;
而ThreadLocal是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源(变量),这样当然不需要对多个线程进行同步了。
所 以,如果你需要进行多个线程之间进行通信,则使用同步机制;如果需要隔离多个线程之间的共享冲突,可以使用ThreadLocal,这将极大地简化你的程 序,使程序更加易读、简洁。

二、线程池

a). 为什么需要线程池
i. 一个线程完成一项任务所需时间为:T1创建线程时间,T2在线程中执行任务的时间,T3销毁线程时间。

ii. 线程池技术正是关注如何缩短或调整T1、T3时间的技术,从而提高程序的性能。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。

iv. 系统启动一个新线程的成本是比较高的,因为涉及与操作系统的交互,在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,优先考虑使用线程池。

v. 线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当run方法执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的Run方法。

vi. JDK1.5以前,开发者必须手动实现自己的线程池,JDK1.5开始,Java内建支持线程池

b) 关键类及方法

i. ExecutorService:线程池的接口

ii. Executors:创建各种线程池的工具类

iii. newSingleThreadExecutor()

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
iv. newFixedThreadPool(int n)
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
v. newCachedThreadPool()
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
例1:线程池的使用

public class ThreadPoolTest {

    public static void main(String[] args) {
        //创建单线程的线程池,保证了任务的顺序执行
//      ExecutorService pool = Executors.newSingleThreadExecutor();
        //创建线程数量可变的线程池,且可缓存
//      ExecutorService pool = Executors.newCachedThreadPool();
        //创建固定大小线程的线程池
        ExecutorService pool = Executors.newFixedThreadPool(2);
        Task task = new Task();
        pool.submit(task);
        pool.submit(task);
        pool.submit(task);
        pool.shutdown();

    }

}
class Task implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+","+i);
        }
    }

}
pool-1-thread-1,0
pool-1-thread-1,1
pool-1-thread-1,2
pool-1-thread-1,3
pool-1-thread-1,4
pool-1-thread-1,5
pool-1-thread-1,6
pool-1-thread-1,7
pool-1-thread-1,8
pool-1-thread-1,9
pool-1-thread-2,0
pool-1-thread-1,0
pool-1-thread-1,1
pool-1-thread-1,2
pool-1-thread-1,3
pool-1-thread-1,4
pool-1-thread-1,5
pool-1-thread-1,6
pool-1-thread-1,7
pool-1-thread-1,8
pool-1-thread-1,9
pool-1-thread-2,1
pool-1-thread-2,2
pool-1-thread-2,3
pool-1-thread-2,4
pool-1-thread-2,5
pool-1-thread-2,6
pool-1-thread-2,7
pool-1-thread-2,8
pool-1-thread-2,9

线程池只创建了两个线程来执行这三个线程任务。

例2:定时执行线程

public class ScheduledThreadPoolTest {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
        //延迟1S后执行
//      pool.schedule(new Runnable() {
//
//          @Override
//          public void run() {
//              // TODO Auto-generated method stub
//              for (int i = 0; i < 10; i++) {
//                  System.out.println(Thread.currentThread().getName() + ","
//                          + i);
//              }
//
//          }
//      }, 1, TimeUnit.SECONDS);

        //延迟1S后每隔1S执行一次
        pool.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                System.out.println(Thread.currentThread().getName() + ","
                            + new Date());
            }
        }, 1, 1, TimeUnit.SECONDS);
    }
}

你可能感兴趣的:(Java)