Fork me on GitHub
返回顶部
跳到底部

多线程

创建线程

继承Thread类创建线程

步骤:

  1. 继承Thread类,重写run()方法,run()方法体为线程执行体。
  2. 创建Thread类的实例,即创建了线程对象。
  3. 调用线程对象的start()方法启动线程。
public class FirstThread extends Thread {

    private int i;
    @Override
    public void run() {
        for (;i<100;i++) {
            System.out.println(getName()+" "+i);
        }
    }
    public static void main(String[] args){
        for (int i=0;i<100;i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
            if (i == 20) {
                new FirstThread().start();
                new FirstThread().start();
            }
        }
    }
}

上面程序共有3个线程,显式创建两个,还有一个main线程,是默认线程。当Java程序开始运行后,程序至少会创建一个主线程,主线程的线程执行体不是有run()方法确定的,而是由main()方法确定的,main()方法的方法体代表主线程的线程执行体。

实现Runnable接口

步骤:

  1. 实现Runnable接口,重写run()方法。
  2. 创建Runnable实现类的实例,并以其作为Thread的构造器参数target来创建Thread对象,该对象才是真正的线程对象。
public class SecondThread implements Runnable {

    private int i;

    @Override
    public void run() {
        for (;i<100;i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
    public static void main(String[] args) {
        for (int i=0;i<100;i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
            if (i == 20) {
                SecondThread secondThrea=new SecondThread();
                new Thread(secondThrea,"新线程1").start();
                new Thread(secondThrea,"新线程2").start();
            }
        }
    }
}

Callable接口实现线程

该接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大。

  • call()方法可以有返回值。
  • call()方法可以声明抛出异常。

Callable不是Runnable的子接口,不能作为Thread的target。Java5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该接口也实现了Runnable接口,它可以作为Thread类的target。

下面是使用Callable接口实现线程的方式。

List<Integer> list=new ArrayList<>(Arrays.asList(1,2,3,4,5));
FutureTask<Integer> futureTask=new FutureTask<>(()->{
   int sum=0;
   for (Integer i:list) {
       sum+=i;
       System.out.println(Thread.currentThread().getName()+"遍历的集合中的值:"+i);
   }
    return sum;
});
new Thread(futureTask).start();
try {
    System.out.println("线程返回值sum="+futureTask.get());
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}

运行结果如下:

Thread-0遍历的集合中的值:1
Thread-0遍历的集合中的值:2
Thread-0遍历的集合中的值:3
Thread-0遍历的集合中的值:4
Thread-0遍历的集合中的值:5
线程返回值sum=15

实现Runnable、Callable接口创建线程的优缺点:
1、还可以继承其它类
2、多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况。
劣势是:
编程稍微复杂,要访问当前线程,必须使用Thread.currentThread()方法。

线程的生命周期

当线程被创建并启动后,它既不是一启动就进入执行状态,也不是一直处于执行状态,在线程的生命周期中,他要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,他不可能一直霸占着CPU独自运行,CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。

线程.PNG

新建和就绪状态

当用new关键字创建一个线程之后,该线程就处于新建状态,此时他和其他Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程没有表现出任何线程的动态特征,程序不会执行线程的线程执行体。
当线程对象调用start()方法之后,该线程就处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,只是表示该线程可以运行了。至于线程何时开始运行,取决于JVM里的线程调度器。

运行和阻塞状态

如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程就处于运行状态。
当发生如下情况时,线程将会进入阻塞状态:

  • 线程调用sleep()方法主动放弃所占用的处理器资源。
  • 线程调用了一个阻塞IO方法,在该方法返回之前,该线程被阻塞。
  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
  • 线程在等待某个通知(notify)。
  • 程序调用了线程的suspend()方法将该线程挂起。

针对以上几种情况,当发生如下特定的情况是可以解除上面的阻塞,让线程重新进入就绪状态

  • 调用sleep()方法的线程经过了特定的时间。
  • 线程调用的阻塞式IO方法已经返回。
  • 线程成功地获取了试图取得的同步监视器。
  • 线程正在等待某个通知时,其他线程发出一个通知。
  • 处于挂起状态的线程被调用了resume()方法。

线程死亡

  1. run()或call()方法执行完成,线程正常结束。
  2. 线程抛出一个未捕获的Exception或Error。
  3. 直接调用该线程的stop()方法来结束线程。

控制线程

join线程

Thread提供了让一个线程等待另一个线程完成的方法-join()。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的线程执行完为止。

public class JoinThread extends Thread {
    public JoinThread(String name) {
        super(name);
    }

    public void run() {
        for (int i=0;i<5;i++){
            System.out.println(getName()+" "+i);
        }
    }
    public static void main(String[] args) throws Exception{
        for (int i=0;i<50;i++){
            if(i==20){
                JoinThread jt=new JoinThread("被joi的线程");
                jt.start();
                jt.join();
            }
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}

执行结果:

...
main 19
被joi的线程 0
被joi的线程 1
被joi的线程 2
被joi的线程 3
被joi的线程 4
main 20
....

后台线程

有一种线程,它是在后台运行的,它的任务是为其他线程提供服务,这种线程成为后台线程(Daemon Thread)。JVM的垃圾回收线程就是典型的后台线程。
后台线程有个特称:所有的前台线程都死亡,后台线程就会自动死亡
调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。

线程睡眠

如果需要让正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep(long mills)方法来实现。当当前线程调用sleep()方法进入阻塞状态后,在其睡眠期间即使系统中没有其它可执行的线程,该线程也不会获得执行的机会。因此sleep()方法常用来暂停程序。

线程让步

yield()方法是一个和sleep()方法相似的方法,它也是Thread类提供的一个静态方法,它也可以让当前正在执行的程序暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。

改变线程的优先级

Thread类提供了setPriority(int priority)方法来改变线程的优先级,priority可以是一个整数,范围1~10,也可以使用如下三个静态常量:

/**
 * The minimum priority that a thread can have.
 */
public final static int MIN_PRIORITY = 1;

/**
 * The default priority that is assigned to a thread.
 */
public final static int NORM_PRIORITY = 5;

/**
 * The maximum priority that a thread can have.
 */
public final static int MAX_PRIORITY = 10;

线程同步

synchronized实现不同线程之间的同步

Java使用synchronized关键字来实现代码在不同线程之间同步的。
synchronized的用法另起一篇做说明。

同步锁ReentrantLock

lock(),unlock()

使用Lock对象也可以实现同步效果,Lock是一个接口,它有ReentrantLock实现类。

Lock lock=new ReentrantLock();

与synchronized(obj),当前线程调用lock的lock()方法,表示当前线程占用lock对象,一旦占用,其它线程就不能占用了。与synchronized不同的是,一旦synchronized同步的块或方法结束,就会自动释放对obj对象的占用。lock必须调用unlock()方法进行手动释放锁。为了保证锁的释放,往往把unlock()放在finally里执行。

public class LockTest {

    public static void main(String[] args) {
        Lock lock=new ReentrantLock();

        Thread thread1 = new Thread(){
            @Override
            public void run(){
                System.out.println("线程1启动,试图占有对象lock");
                lock.lock();
                System.out.println("线程1已占有对象");
                System.out.println("线程1进行5秒钟的业务操作");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("线程1结束,释放对象lock");
                    lock.unlock();
                }
            }
        };
        thread1.start();
        try {
            //先让线程1运行2秒
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread thread2 = new Thread(){
            @Override
            public void run() {
                System.out.println("线程2启动,试图占有对象lock");
                lock.lock();
                System.out.println("线程2已经占有对象lock");
                System.out.println("线程2进行5秒的业务操作");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("线程2结束,释放对象lock");
                    lock.unlock();
                }
            }
        };
        thread2.start();
    }
}

运行结果如下:

线程1启动,试图占有对象lock
线程1已占有对象
线程1进行5秒钟的业务操作
线程2启动,试图占有对象lock
线程1结束,释放对象lock
线程2已经占有对象lock
线程2进行5秒的业务操作
线程2结束,释放对象lock

可以看到线程1占有对象lock,进行业务处理期间,线程2是获取不到锁的,直到线程1结束,释放锁。

tryLock()

synchronized是不占用到锁,就一直试图占用下去,与synchronized不一样,Lock接口提供了一个trylock()方法。trylock()方法会在指定的时间范围内试图占用,占用成功了,就OK。如果在指定时间里没有占用到,就不会再去等待。

public class LockTest {

    public static void main(String[] args) {
        Lock lock=new ReentrantLock();
        Thread thread1 = new Thread(){
            @Override
            public void run(){
                System.out.println("线程1启动,试图占有对象lock");
                lock.lock();
                System.out.println("线程1已占有对象");
                System.out.println("线程1进行5秒钟的业务操作");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("线程1结束,释放对象lock");
                    lock.unlock();
                }
            }
        };
        thread1.start();
        try {
            //先让线程1运行2秒
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread thread2 = new Thread(){
            @Override
            public void run() {
                System.out.println("线程2启动,试图占有对象lock");
//                lock.lock();
                boolean locked= false;
                try {
                    locked = lock.tryLock(1, TimeUnit.SECONDS);
                    if (locked) {
                        System.out.println("线程2已经占有对象lock");
                        System.out.println("线程2进行5秒的业务操作");
                        Thread.sleep(5000);
                    }else{
                        System.out.println("线程2经过1秒钟的的努力,没有占用到对象,放弃占有");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    if (locked) {
                        System.out.println("线程2结束,释放对象");
                    }else{
                        System.out.println("线程2没占用到对象,直接结束");
                    }
                }
            }
        };
        thread2.start();
    }
}

打印结果如下:

线程1启动,试图占有对象lock
线程1已占有对象
线程1进行5秒钟的业务操作
线程2启动,试图占有对象lock
线程2经过1秒钟的的努力,没有占用到对象,放弃占有
线程2没占用到对象,直接结束
线程1结束,释放对象lock

可以看到线程试图占用对象,但经过1秒钟还没有占用到,就放弃了。

线程通信

多线程之间通过被加锁对象的wait()notify()方法进行通信。
比如有A,B两个线程处于运行状态,A线程先获得对象obj的锁,如果在A线程体内,obj调用了wait(),那么A线程就会暂时放弃对obj对象的锁的占有,B线程就会获得对obj对象的锁;B线程执行期间如果对象obj调用了notify()方法,那么处于等待状态的线程,也即B线程,就会重新获得对obj对象的锁。
还有一个notifyAll()方法,顾名思义,就是唤醒所有等待状态的线程。

注意:wait,notify,nitifyAll并不是Thread线程上的方法,它们是Object上的方法。
因为所有的Object都可以被用来作为同步对象,所以准确的讲,wait和notify是同步对象上的方法。
wait()的意思是: 让占用了这个同步对象的线程,临时释放当前的占用,并且等待。 所以调用wait是有前提条件的,一定是在synchronized块里,否则就会出错。
notify() 的意思是,通知一个等待在这个同步对象上的线程,你可以苏醒过来了,有机会重新占用当前对象了。
notifyAll() 的意思是,通知所有的等待在这个同步对象上的线程,你们可以苏醒过来了,有机会重新占用当前对象了。

死锁

当业务比较复杂,多线程应用里有可能会发生死锁。

  1. 线程1 首先占有对象obj1,接着试图占有对象obj2
  2. 线程2 首先占有对象obj2,接着试图占有对象obj1
  3. 线程1 等待线程2释放对象obj2
  4. 与此同时,线程2等待线程1释放对象obj1
public class DeadLockTest {

    public static void main(String[] args) {
        Object obj1=new Object();
        Object obj2=new Object();

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (obj1) {
                    System.out.println("thread1 已占有obj1");
                    try {
                        //模仿线程1在处理业务
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("thread1 试图占有obj2,等待中.......");
                    synchronized (obj2) {
                        System.out.println("不能输出此句,则发生死锁");
                    }
                }

            }
        };
        thread1.start();
        Thread thread2 = new Thread(){
            @Override
            public void run() {
                synchronized (obj2){
                    System.out.println("thread2 已占有obj2");
                    try {
                        //模仿线程2在处理业务
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println("thread2 试图占有obj1,等待中........");
                    synchronized (obj1){
                        System.out.println("不能输出此句,则发生死锁");
                    }
                }
            }
        };
        thread2.start();
    }
}

运行结果:

thread1 已占有obj1
thread2 已占有obj2
thread2 试图占有obj1,等待中........
thread1 试图占有obj2,等待中.......

线程池

系统启动一个新的线程的成本是比较高的,因为它涉及与操作系统交互。使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,就更应该考虑使用线程池。除此之外,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数参数可以控制系统中并发线程数不超过此数。

在Java5以前,开发者必须手动实现自己的线程池;从Java5开始,Java内建支持线程池。Java5新增了一个Executors工厂类来产生线程池。

public class TestThread {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(6);
        Runnable target=()->{
            for(int i=0;i<100;i++){
                System.out.println(Thread.currentThread().getName()+"的i值为:"+i);
            }
        };
        pool.submit(target);
        pool.submit(target);
        pool.shutdown();
    }
}

ForkJoinPool

public class PrintTask extends RecursiveAction {

    private static final int THREADHOLD =50;
    private static final long serialVersionUID = 5204454735323133216L;
    private int start;
    private int end;
    public PrintTask(int start,int end){
        this.start=start;
        this.end=end;
    }

    @Override
    protected void compute() {
        if (end - start < THREADHOLD) {
            for(int i=start;i<end;i++) {
                System.out.println(Thread.currentThread().getName()+"的i值:"+i);
            }
        }else {
            int middle=(start+end)/2;
            PrintTask left=new PrintTask(start,middle);
            PrintTask right=new PrintTask(middle,end);
            left.fork();
            right.fork();
        }
    }

    public static void main(String[] args) {
        ForkJoinPool pool=new ForkJoinPool();
        pool.submit(new PrintTask(0,300));
        try {
            pool.awaitTermination(2, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        pool.shutdown();
    }
}

原子操作

所谓原子操作是不可中断的操作,比如赋值操作:int i=3;
原子操作是线程安全的,但i++这个行为,是由3个原子操作组成:
步骤1: 取i的值;
步骤2: 计算i+1;
步骤3: 把新值赋给i;
这三个步骤,每一步都是原子操作,但合在一起,就不是原子操作了。所以可能发生这样的情况,一个线程在步骤1取得i的值后,还没来得及进行步骤2,另一个线程就取得i的值了。所以i++这个操作不是线程安全的。

解决方案:使用AtomicInteger类。
Java6新增了一个包java.util.concurrent.atomic,里面有各种原子类,比如AtomicInteger,它提供了各种自增、自减原子性的,也就是线程安全的。

下面例子演示了这两种情况:

public class AtomicTest {

    private static int value=0;
    private static AtomicInteger atomicInteger=new AtomicInteger();

    public static void main(String[] args) {
        int num=100000;
        Thread[] threads=new Thread[num];
        for (int i=0;i<num;i++) {
            Thread thread = new Thread(){
                @Override
                public void run() {
                    value++;
                }
            };
            thread.start();
            threads[i]=thread;
        }
        //在main线程里join这些线程,等待这些线程全部执行结束,在执行main线程
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.printf("%d个线程进行value++后,value的值变为:%d\n,",num,value);

        Thread[] threads1=new Thread[num];
        for (int i=0;i<num;i++) {
            Thread thread = new Thread(){
                @Override
                public void run() {
                    atomicInteger.incrementAndGet();
                }
            };
            thread.start();
            threads1[i]=thread;
        }
        //在main线程里join这些线程,等待这些线程全部执行结束,在执行main线程
        for (Thread thread : threads1) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.printf("%d个线程进行atomicInteger.intValue()操作后,atomicInteger的值变为:%d\n",num,atomicInteger.intValue());
    }
}

上面程序输出结果为:

100000个线程进行value++后,value的值变为:99995
100000个线程进行atomicInteger.intValue()操作后,atomicInteger的值变为:100000

线程相关的类

ThreadLocal类

ThreadLocal代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,从线程的角度看,就好像每个线程都完全拥有该变量一样。

class Account{
    private ThreadLocal<String> name=new ThreadLocal<>();
    public Account(String str){
        name.set(str);
        System.out.println("-----"+name.get());
    }

    public String getName() {
        return name.get();
    }

    public void setName(String str) {
        name.set(str);
    }
}

class MyTest extends Thread {
    private Account account;

    public MyTest(Account account, String name) {
        super(name);
        this.account=account;
    }

    @Override
    public void run() {
        for (int i=0;i<5;i++) {
            if (i == 3) {
                account.setName(getName());
            }
            System.out.println(account.getName()+"账户的i值:"+i);
        }
    }
}
public class ThreadLocalTest {

    public static void main(String[] args) {
        Account at=new Account(Thread.currentThread().getName()+"线程");
        
        new MyTest(at,"线程A").start();

        new MyTest(at,"线程B").start();
    }
}

输出结果为:

-----main线程
null账户的i值:0
null账户的i值:1
null账户的i值:2
线程A账户的i值:3
线程A账户的i值:4
null账户的i值:0
null账户的i值:1
null账户的i值:2
线程B账户的i值:3
线程B账户的i值:4

上面有三个线程,隐式的main,两个MyTest线程,三个线程都拥有account这个实例变量,但account中的name是ThreadLocal类型的,所以每个线程都拥有该变量name的副本,各个线程对name的操作互不影响。需要注意的是在main主线程上为name赋值为“主线程”,但并不影响另两个线程name的值。

线程安全的集合

从Java5开始,在java.util.concurrent包下提供了大量支持高效并发访问的集合接口和实现类。

  • 以Concurrent开头的集合类,如ConcurrentHashMap,ConcurrentSkipListMap。
  • 以CopyOnWrite开头的集合类,如CopyOnWriteArrayList。

其中以Concurrent开头的集合类代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是安全的,但读取操作不必锁定。

CopyOnWriteArrayList执行写入操作时需要频繁地复制数组,性能比较差,但由于读操作与写操作不是操作同一个数组,而且读操作也不需要加锁,因此读操作就很快、很安全。CopyOnWriteArrayList适合用在读取操作远远大于写入操作的场景中,如缓存等。

posted @ 2018-05-15 09:19  sqmax  阅读(162)  评论(0编辑  收藏  举报