Java并行程序基础知识

一、线程简介

  1. 进程:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体,在当代面向线程设计的计算机结构中,进程是线程的容器,程序是指令、数据及其组织形式的描述,进程是程序的实体;
  2. 线程是轻量级进程,是程序的最小执行单位,使用多线程进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程;
  3. 线程的状态:线程的所有状态都在Thread中的State枚举中定义:

    • NEW 刚刚创建线程,还没开始执行;
    • RUNNABLE 线程正在执行
    • BLOCKED 线程遇到synchronized同步块,会进入阻塞状态,线程暂停直到获得请求的锁
    • WAITING 无限期等待
    • TIMED_WAITING 有时限的等待
    • TERMINATED 线程执行完毕

二、线程的基本操作

2.1 新建线程

1. 使用匿名内部类新建线程
public class Demo {
    public static void main(String[] arg){
        Thread t1 = new Thread(){
            @Override
            public void run() {
                System.out.println("线程执行啦!");
            }
        };
        t1.start();
    }
}

我们通过匿名内部类的方式重载Thread的run方法,start()就会执行run()内的代码。这里要注意t1.run();和t1.start();的区别t1.run();只会在当前线程中串行执行run()中的代码。

2. 通过构造方法新建线程

Thread类中有一个非常重要的构造方法public Thread(Runnable target),单纯使用接口定义Thread,避免重载Thread.run(),实现与 1 同样的操作。

public class Demo implements Runnable{
    public static void main(String[] arg){
        Thread t1 = new Thread(new Demo());
        t1.start();
    }
    @Override
    public void run() {
        System.out.println("线程执行啦!");
    }
}

2.2 终止线程

在JDK中,Threa提供stop()方法停止线程,但是这个方法被标记为废弃的方法,因为通过stop()方法终止线程,会立即释放线程所持有的所有锁,会导致对象的不一致性。如果我们需要停止一个线程,可以定义一个标记变量stopme,用于指示线程是否需要退出,示例如下:

public class MyThread extends Thread {
    volatile boolean stopme = false;

    public static class User{
        private String name;
        private String adress;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getAdress() {
            return adress;
        }

        public void setAdress(String adress) {
            this.adress = adress;
        }
    }

    User u = new User();
    public void stopme() {
        stopme =true;
    }

    @Override
    public void run() {
        while (true){
            if(stopme){
                System.out.println("退出线程");
                break;
            }
            synchronized (u){
                u.setName("simon");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                u.setAdress("GZ");
            }
            Thread.yield();
        }
    }
}

使用这种方式退出,不会是对象u的状态出现错误。

2.3 线程中断

在Java中为了完善线程的退出功能,提供一套自有的线程协作机制——线程中断,与线程中断有关的方法如下:

public void Thread.interrupt()                  //中断线程
public boolean Thread.isInterrupted()           //判断线程是否被中断
public static boolean Thread.interrupted()      //判断线程是否被中断,并清除当前中断状态

Thread.interrupt()方法中断,线程不会立刻停下来,比如死循环体。可以通过isInterrupted()方法来判断是否跳出循环体

@Override
    public void run() {
        while (true){
            if(Thread.currentThread().isInterrupted()){
                break;
            }
            synchronized (u){
                u.setName("simon");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                u.setAdress("GZ");
            }
            Thread.yield();
        }
    }

2.4 线程等待wait和通知notify

为了支持多线程间的协作,JDK提供了两个非常重要的接口线程等待wait()通知notify()方法,这两个方法在Object类,任何对象都可以的调用这两个方法,它两是配套的,一个线程调用obj.wait()处于等待状态,需要其他线程调用obj.notify()来唤醒。

public final void wait() throw InterruptedException
public final native void notify() //唤醒等待队列中的任意一个
public final native void notifyAll() //唤醒等待队列所有

注意:

  • wait() 必须包含在 synchronzied 语句中,无论是 wait() 还是 notify() 都需要先获得目标对象的一个监视器。(这两个方法都是执行后立刻释放监视器,防止其他等待对象线程因为该线程休眠而全部无法正常执行)

2.5 线程挂起线程挂起suspend和继续执行resume

线程挂起suspend()和线程继续执行resume()这两个方法在JDK中被标记为废弃的方法,不推介使用。
1. suspend()会导致休眠线程的所有资源都不会被释放,直到执行 resume() 方法。(如果两个方法,后者执行于前者之前,则线程很难有机会被继续执行)
2. 对于被挂起的线程,其状态是Runnable,这会严重影响到我们对于系统当前状态的判断

2.6 等待线程结束join和谦让yield

1. join()方法会阻塞当前线程,直到目标线程结束,或者到达阻塞时间。 同时,join()方法的本质是让在当前线程对象实例上调用线程的wait()方法
public final void join() throws InterruptedException; //无限等待
public final void join(long millis) throws InterruptedException;//等待一定时间
2.当某个线程不是那么重要,或者优先级别较低,可以在适当时候调用 Thread.yield(),给予其他重要线程更多工作机会。yield会使当前线程让出CPU,但还是会进行CPU资源的抢夺

三、volatile与Java内存模型(JMM)

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的(数据可见性)。
2. 禁止进行指令重排序(数据有序性)。

那么volatile可以保证原子性吗?看下面的例子:

public class Demo {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Demo demo = new Demo();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        demo.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(demo.inc);
    }
}

大家想一下这段程序的输出结果是多少?也许有些朋友认为是10000。但是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,这样就会出现上面的问题。

四、线程组

如果线程数量很多,并且功能分配明确,就可以将相同功能的线程放在一个线程组,以书中代码为例:

public class ThreadGroup implements Runnable {

    public static void main(String[] args) {
        //创建一个叫"PrintGroup"的线程组
        ThreadGroup tg = new ThreadGroup("PrintGroup");
        //加入线程组
        Thread t1 = new Thread(tg, new ThreadGroup(), "T1");   
        Thread t2 = new Thread(tg, new ThreadGroup(), "T2");
        t1.start();
        t2.start();
        //由于线程是动态的,activeCount()获得活动线程总数的估算值
        System.out.println("活动线程总数 = " + tg.activeCount());
        //打印出线程组的线程信息
        tg.list();
        //这个方法与Thread.stop()方法遇到问题一样
        tg.stop();
    }

    @Override
    public void run() {
        //获取线程组名称
        String groupAndName = Thread.currentThread().getThreadGroup().getName()
                + "---" + Thread.currentThread().getName();
        while (true) {
            System.out.println("I am " + groupAndName);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

五、守护线程

守护线程是一种特殊的线程,是系统的守护者,在后台完成一些特殊的服务。比如垃圾回收线程,JIT线程都可理解为守护线程。与守护线程相对的叫用户线程,当一个Java应用内只有守护线程时,Java虚拟机就会自然退出。

public class Demo implements Runnable{
    public static void main(String[] arg){
        Thread t1 = new Thread(new Demo());
        //设置守护线程
        t1.setDaemon(true);
        //设置守护线程必须在线程start()之前设置,否则会得到一个IllegalThreadStateException,但程
        //序和线程依然可以正常执行,只是线程被当作用户线程而已
        t1.start();
        t1.interrupt();
    }
    @Override
    public void run() {
        while (true){
            System.out.println("I am alive!!");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

六、线程优先级

Java中线程可以有自己的优先级。由于线程的优先级调度和底层操作系统有密切关系,在各个平台表现不一,无法精准控制,低优先级的可能一直抢不到资源,处于饥饿状态,在要求严格的场合,需要在应用层自己解决线程调度的问题。
在Java中,使用 1 到 10 表示线程优先级。一般可以使用三个内置的静态标量表示:

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

七、syschronized关键字

syschronized用法如下:
1. 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
2. 直接作用于实例对象:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
3. 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

八、隐蔽的错误

  1. 线程不安全的容器:ArrayList、HashMap 等。改用线程安全的容器,如 Vector、ConcurrentHashMap。
  2. 不变对象加锁,导致对于临界区代码控制出现问题。例如Integer。
public class Demo implements Runnable{
    public static Integer i = 0;
    static Demo demo = new Demo();
    public static void main(String[] arg) throws InterruptedException {
        Thread t1 = new Thread(demo);
        Thread t2 = new Thread(demo);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.print(i);
    }
    @Override
    public void run() {
        for(int j = 0;j < 10000;j++){
            synchronized (i){
                i++;
            }
        }
    }
}

这样不能获得预期的结果,典型的加锁错误。i++的本质是创建一个新的Integer对象,并将引用赋值给i,因此每次锁的对象都不一样。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值