Java 线程和进程,并发解决之synchronized

来源:互联网 发布:富途牛牛安全吗 知乎 编辑:程序博客网 时间:2024/05/20 08:24
  1. 什么是进程?
    程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。
    在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

  2. 有了进程为什么还要线程?

    进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:
    1. 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
    2. 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依
        赖于输入的数据,也将无法执行。

  3. 线程和进程的区别
    1. 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
    2. 线程是进程的一个实体, 是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
    3. 一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

  4. 线程的优点

    因为要并发,我们发明了进程,又进一步发明了线程。只不过进程和线程的并发层次不同:进程属于在处理器这一层上提供的抽象;线程则属于在进程这个层次上再提供了一层并发的抽象。如果我们进入计算机体系结构里,就会发现,流水线提供的也是一种并发,不过是指令级的并发。这样,流水线、线程、进程就从低到高在三个层次上提供我们所迫切需要的并发!

    除了提高进程的并发度,线程还有个好处,就是可以有效地利用多处理器和多核计算机。现在的处理器有个趋势就是朝着多核方向发展,在没有线程之前,多核并不能让一个进程的执行速度提高,原因还是上面所有的两点限制。但如果讲一个进程分解为若干个线程,则可以让不同的线程运行在不同的核上,从而提高了进程的执行速度。
    总结线程优点如下:

    (1)多线程技术使程序的响应速度更快 ,用户界面可以在进行其它工作的同时一直处于活动状态;

    (2)当前没有进行处理的任务时可以将处理器时间让给其它任务;

    (3)占用大量处理时间的任务可以定期将处理器时间让给其它任务;

    (4)可以随时停止任务(在Android的API中没有提供stop方法,在stop方法中只是抛出一个异常去停止);

    (5)可以分别设置各个任务的优先级以优化性能。

  5. 线程的缺点

    (1)等候使用共享资源时造成程序的运行速度变慢。这些共享资源主要是独占性的资源 ,如打印机等。

    (2)对线程进行管理要求额外的 CPU开销。线程的使用会给系统带来上下文切换的额外负担。当这种负担超过一定程度时,多线程的特点主要表现在其缺点上,比如用独立的线程来更新数组内每个元素。

    (3)线程的死锁。即较长时间的等待或资源竞争以及死锁等多线程症状。

    (4)对公有变量的同时读或写造成数据混乱,比如启动了A,B两个线程操作同一个变量count,有可能你想让A先操作变量,但是当A,B线程并发的时候,有可能B先去操作count,A,B也可能交换着操作count。导致最后的结果不是预期结果

  6. 并发的解决办法
    synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种: 
    1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码, 作用的对象是调用这个代码块的对象(实例); 
    2. 修饰一非静态个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象(实例); 
    3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象; 
    4. 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象(实例)。

    温馨提示:建议在看下面的例子之前先去看看我的一片博文“多线程之原子性,可见性,有序性,并发问题解决”.本文会引用改文章为A文在这篇文章中详细解释了为什么会出现并发问题,并发问题发生的时候是什么状况!搞懂这些问题之后再来看看下面的例子,就很简单了。
    针对以上四种修饰详解如下:

    定义了一个Person类,在这个类中写了四个方法,对这四个方法用了synchronized的四种用法。这四个方法做的是同一件事儿,就是让count做+1操作。每次调用该方法count就+1.我们会启动10个线程来调用同一个方法,也就是说我们count最后的值应该是10.这是我们的预期结果,但是真正的结果是啥呢?我们拭目以待!

/**
 * Created by PICO-USER dragon on 2017/2/23. */public class Person {    private static int count = 0;    private String name;    public Person(String name) {        this.name = name;    }    public String getName() {        return name;    }    /**     * 普通方法没有加synchronized修饰,存在并发问题     *     * @param person     */    public void say(Person person) {        count++;        System.out.print("say  person :" + person.getName() + " " + Thread.currentThread().getName() + " count :" + count + "\n");    }    public void say1_1(Person person) {        //这儿锁定的是this,代表着Person某一个实例的锁,只对这个实例互斥。        synchronized (this) {            count++;            System.out.print("say1  person :" + person.getName() + " " + Thread.currentThread().getName() + " count :" + count + "\n");        }    }    //非静态方法属于实例所有,所以这儿跟say1_1方法一样,也是Person某一个实例的锁    public synchronized void say1(Person person) {        count++;        System.out.print("say1  person :" + person.getName() + " " + Thread.currentThread().getName() + " count :" + count + "\n");    }    //静态方法,归类所有,是Person这个类的锁,对所有Person类的实例互斥    public static synchronized void say2(Person person) {        count++;        System.out.print("say2  person :" + person.getName() + " " + Thread.currentThread().getName() + " count :" + count + "\n");    }    public void say2_2(Person person) {        //这儿是Person.class。也就是说跟say2一样。        synchronized (Person.class) {            System.out.print("say3  person :" + person.getName() + " " + Thread.currentThread().getName() + " count :" + count + "\n");        }    }}

注意:一定要搞清楚,类和类的实例是不一样的。如果不能搞清楚这一点,是没法搞懂synchronized的。还有一点,静态成员变量和静态方法归类所有。这些是很基础的Java知识了,一定要搞清这些细节。一个类可以有很多很多的实例,比如说人类,地球上几十亿人口,全都是人类的实例。

上面的四个方法,分别是synchronized的四种不同的用法,其中say1和say1_1方法都是Person这个类的实例的锁,所以它只是针对某一个实例有互斥作用。现在我们来看看调用say1方法的代码和运行结果。然后再来解释。

现在我们来看看调用say1方法的代码和运行结果


再定义一个线程类,用于访问Person类中的方法,这儿我们先访问没有并发处理的say方法。看看运行结果

/** * Created by PICO-USER dragon on 2017/2/23. */public class MyThread extends Thread {    private Person person;    public MyThread(Person person) {        this.person = person;    }    @Override    public void run() {        super.run();        if (person != null) {            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }            //调用没有并发处理的say方法            person.say(person);        }    }}

public class Run {    public static void main(String[] args0) {        //定义了一个Person类的实例,这个人的名字叫dragon        Person person = new Person("dragon");        //启动了10个线程,传入的实例是同一个,相当于dragon同时在不同的线程中调用同一个方法,让Person类中的count+1操作        for (int i = 0; i < 10; i++) {            new MyThread(person).start();        }    }}
结果:


可以看出来,并发问题出现的很严重。并没有得到我们预期的结果!至于为什么会出现这个情况,在A文中已经写得很详细了。那就需要去A文中找答案了!这儿就不细说了。


现在我们Main类不变,改改线程类MyThread中调用的方法为say1

@Overridepublic void run() {    super.run();    if (person != null) {        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        //改为调用并发处理的say1        person.say1(person);    }}
结果:


现在可以看到我们得到了预期的结果,这是因为我们加了synchronized修饰say1方法,并且是对Person类的实例dragon上锁。dragon在不同的线程中来访问该方法的时候,必须保证同一时间只有同一个线程在访问该代码。其他的线程就在外面等着,直到正在执行该方法中的代码的线程执行完了,释放掉该对象的锁.正在等待的线程才能去争取dragon的锁,拿到锁的线程就能进来访问代码。


现在我们还是调用say1方法,但是我们改改Main类中的代码,改为不同的对象在不同的线程中去访问该方法,再看看是什么请款。


结果表明并发的问题发生了?为什么加了synchronized还是会放生呢?很简单,前面我们着重讲到,say1是对Person类的实例上锁,也就是说对同一个dragon在不同的线程中起到互斥的作用。但是,现在是10个不同的dragon。比如说现在dragon0正在访问say1,还没用完呢。这时候dragon1来了,因为这个方法不是是归对象所有,也就是说dragon0有一个say1方法。dragon1也有一个say1方法。这个锁对他们两个来说,不是互斥的,并没有什么作用。所以synchronized并没有起到作用。并发的问题没有解决。

say1_1根say是一样的。网友可以根据say1的测试方法测试进行验证。这儿就不再多说了。下面随便说一下say2和say2_2,对Person类上锁的情况。


更改线程类MyThread中调用方法的代码。

改为调用并发处理并且是对Person类上锁的say2

@Overridepublic void run() {    super.run();    if (person != null) {        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        //改为调用并发处理并且是对Person类上锁的say2        person.say2(person);    }}

再改一下Main类中启动线程的代码:

同一个对象,在不同的10个线程中去访问say2方法。

public class Run {    public static void main(String[] args0) {        //定义了一个Person类的实例,这个人的名字叫dragon        Person person = new Person("dragon");        //启动了10个线程,传入的实例是同一个,相当于dragon同时在不同的线程中调用同一个方法,让Person类中的count+1操作        for (int i = 0; i < 10; i++) {            //   Person person = new Person("dragon" + i);            new MyThread(person).start();        }    }}
运行结果:


可以看到,是预期结果,因为我们是对Person类上锁。不论你是不是同一个对象,只要你是Person类的子类,Person类就能管理。就好比:现在父亲说了,要将100年的功力传给他的一堆儿子,每一次传功力都必须是父亲亲手亲为,并且同一时间只能为一个人传功,否则大家都得死。现在上面的例子就是老大会分身术,变成两个dragon跑去找他老子,他老子根本不会管是不是同一个人,他只知道,他同一时间只能给一个人传功。不论是不是同一个人过来要功力,都会让他们排队。

   接下来我们再看看多个儿子同时跑去找他老子的例子;其实不用改都知道肯定是解决并发的。

   其他代码不动,只需要更改Main类中的代码,改成10个儿子就行:

public class Run {    public static void main(String[] args0) {        /*//定义了一个Person类的实例,这个人的名字叫dragon        Person person = new Person("dragon");*/        //启动了10个线程,传入的实例是同一个,相当于dragon同时在不同的线程中调用同一个方法,让Person类中的count+1操作        for (int i = 0; i < 10; i++) {            Person dragon = new Person("dragon" + i);            new MyThread(dragon).start();        }    }}
结果:


ok,这也是预料之中的结果,我这儿就不多说了,say2_2跟say2是同样的效果。这儿也不讲了。



2 0