Java高并发编程基础

注:该篇文章已与我的个人博客同步更新。欢迎移步https://cqh-i.github.io/体验更好的阅读效果。

进程

进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。

线程

线程是指进程中的一个执行路径,一个进程中可以运行多个线程。同一类的线程共享代码和数据空间, 每个线程使用其所属进程的栈空间。线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

进程和线程的区别

主要区别是,同一进程内的多个线程会共享部分状态,多个线程可以读写同一块内存,一个进程无法直接访问另一进程的内存。同时,每个线程还拥有自己的寄存器和栈,它的兄弟线程可以读写这些栈内存。可以表现在以下几方面上:

1.地址空间和其他资源:进程间相互独立,同一进程的各线程间共享。某进程内的线程在其他进程内不可见。

2.通信:进程间通信IPC包括管道,信号量,共享内存,消息队列,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要线程同步和互斥手段的辅助,以保证数据的一致性。

3.调度和切换:线程上下文切换比进程上下文切换快得多。

Java synchronized(对象锁)和synchronized static(类锁)

  • 对象锁: synchronized是对类的当前实例(当前对象)进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块(不包括synchronized static 修饰的方法)。

  • 类锁: 由于一个类不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。所以,一旦一个静态的方法被申明为synchronized,此类所有的实例化对象在调用此方法,共用同一把锁,我们称之为类锁。synchronized static 是限制多线程中该类的所有实例同时访问 JVM 中该类所对应的代码块(synchronized static 修饰的代码),锁在该类的class对象(xxx.class)。

    类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。

    Java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个Java对象,只不过有点特殊而已。由于每个Java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已

实际上,在类中如果某方法或某代码块中有 synchronized,那么在生成一个该类实例后,该实例也就有一个监视块,防止线程并发访问该实例的synchronized保护块,而 synchronized static则是该类的所有实例公用的一个监视块,这就是他们两个的区别。

对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。

Java高并发编程基础_第1张图片

下面通过例子来加深理解

public class ThreadDemo {

    public synchronized void syncM1() {
        System.out.println(Thread.currentThread().getName() + " syncM1()方法开始执行, 将休眠5秒 ");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("休眠结束,syncM1()结束");
    }

    public synchronized void syncM2() {
        System.out.println(Thread.currentThread().getName() + " syncM2()方法执行了");
    }

    public synchronized static void syncSM3() {
        System.out.println(Thread.currentThread().getName() + " syncSM3()方法执行了, 将休眠5秒");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("休眠结束,syncSM3()结束");
    }

    public synchronized static void syncSM4() {
        System.out.println(Thread.currentThread().getName() + " syncSM4()方法执行了, 将休眠5秒");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("休眠结束,syncSM4()结束");
    }

    public void m5() {
        System.out.println(Thread.currentThread().getName() + " m5()方法执行了");
    }

}

ThreadDemo有两个synchronized修饰的方法syncM1()syncM2(), 两个synchronized static修饰的方法syncSM3syncSM4 以及一个普通方法m5(), 那么,假如有ThreadDemo类的两个实例x与y,那么下列各组方法被多线程同时访问的情况是怎样的?

  • x.syncM1()x.syncM2

    public static void main(String[] args) {
        ThreadDemo x = new ThreadDemo();
        Thread t1 = new Thread(() -> x.syncM1(), "t1");
        Thread t2 = new Thread(() -> x.syncM2(), "t2");
        t1.start();
        t2.start();
        
        /*
         * new Thread(() -> x.syncM1(), "t1");
         *相当于是下面的写法,上面这种写法是lamada表达式,Java8以后的特性
         * new Thread(new Runnable(){
         *      public void run(){
         *          x.syncM1();
         *      }
         *  }, "t1");
         */
    }
    

    运行结果:

    t1 syncM1()方法开始执行, 将休眠5秒 
    休眠结束,syncM1()结束
    t2 syncM2()方法执行了
    

    运行结果说明了, x.syncM1()x.syncM2不能被同时访问, 因为都是对同一个实例x的synchronized域访问, 多线程中访问x的不同synchronized域不能同时访问, 必须等待锁的释放。

  • x.syncM1()x.syncM1

    public static void main(String[] args) {
        ThreadDemo x = new ThreadDemo();
        Thread t1 = new Thread(() -> x.syncM1(), "t1");
        Thread t2 = new Thread(() -> x.syncM1(), "t2");
        t1.start();
        t2.start();
    }
    

    运行结果:

    t1 syncM1()方法开始执行, 将休眠5秒 
    休眠结束,syncM1()结束
    t2 syncM1()方法开始执行, 将休眠5秒 
    休眠结束,syncM1()结束
    

    运行结果说明了,在多个线程中访问x.syncM1(),因为仍然是对同一个实例,且对同一个方法加锁,所以多个线程中也不能同时访问。(多线程中访问x的同一个synchronized域不能同时访问)

  • x.syncM1()y.syncM1()

运行结果:

t1 syncM1()方法开始执行, 将休眠5秒 
t2 syncM1()方法开始执行, 将休眠5秒 
休眠结束,syncM1()结束
休眠结束,syncM1()结束

运行结果说明了,针对不同实例的,可以同时被访问(对象锁对于不同的对象实例没有锁的约束

  • x.syncSM3()y.syncSM4()

    public static void main(String[] args) {
        ThreadDemo x = new ThreadDemo();
        ThreadDemo y = new ThreadDemo();
        Thread t1 = new Thread(() -> x.syncSM3(), "t1");
        Thread t2 = new Thread(() -> y.syncSM4(), "t2");
        t1.start();
        t2.start();
    }
    

    运行结果:

    t1 syncSM3()方法执行了, 将休眠5秒
    休眠结束,syncSM3()结束
    t2 syncSM4()方法执行了, 将休眠5秒
    休眠结束,syncSM4()结束
    

    运行结果说明了,不能被同时访问。因为类锁synchronized static 是限制多线程中该类的所有实例同时访问 JVM 中该类所对应的静态同步代码块。

  • x.syncM1()ThreadDemo.syncSM3()

    public static void main(String[] args) {
        ThreadDemo x = new ThreadDemo();
        Thread t1 = new Thread(() -> x.syncM1(), "t1");
        Thread t2 = new Thread(() -> ThreadDemo.syncSM3(), "t2");
        t1.start();
        t2.start();
    }
    

    运行结果:

    t1 syncM1()方法开始执行, 将休眠5秒 
    t2 syncSM3()方法执行了, 将休眠5秒
    休眠结束,syncM1()结束
    休眠结束,syncSM3()结束
    

    运行结果说明了,可以被同时访问的,类锁和对象锁控制着不同的区域,它们是互不干扰的。

同步和非同步方法是否可以同时调用

答案是可以被同时调用的。继续使用上面的程序x.syncM1()x.m5()

public static void main(String[] args) {
    ThreadDemo x = new ThreadDemo();
    Thread t1 = new Thread(() -> x.syncM1(), "t1");
    Thread t2 = new Thread(() -> x.m5(), "t2");
    t1.start();
    t2.start();
}

运行结果:

t1 syncM1()方法开始执行, 将休眠5秒 
t2 m5()方法执行了
休眠结束,syncM1()结束

一个同步方法可以调用另外一个同步方法吗?

可以。 一个线程已经拥有某个对象的锁,再次申请的时候仍会得到该对象的锁。也就是说synchronized获得的锁是可重入的

public class ThreadTest {
    public synchronized void m1() {
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2();
    }

    public synchronized void m2() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2");
    }

    public static void main(String[] args) {
        ThreadTest t = new ThreadTest();
        t.m1();
    }
}

运行结果:

m1 start
m2

另外一种情形是子类调用父类的同步方法。

可重入锁(ReentrantLock)原理:

每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。

关于生动形象地讲解可重入锁(ReentrantLock)的实现原理可以参考以下博文:
轻松学习java可重入锁(ReentrantLock)的实现原理

线程异常抛出后,锁会被释放吗?

程序在执行过程中,如果出现异常,默认情况锁会被释放。所以,在并发处理过程中,有异常要多加小心,不然可能会发生不一致的情况。比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。因此要非常小心的处理同步业务逻辑中的异常。

public class T {
    int count = 0;

    synchronized void m() {
        System.out.println(Thread.currentThread().getName() + " start");
        while (true) {
            count++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
            try {
                TimeUnit.SECONDS.sleep(1);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (count == 5) {
                int i = 1 / 0; // 此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
                System.out.println(i);
            }
        }
    }

    public static void main(String[] args) {
        T t = new T();
        Runnable r = new Runnable() {

            @Override
            public void run() {
                t.m();
            }

        };
        new Thread(r, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(r, "t2").start();
    }

}

正常情况下,线程1死循环,线程2永远不会被执行,但是线程1异常抛出后,锁被释放了,线程2就会执行了。

模拟死锁

死锁出现的条件一般就是线程之间互相等待对方释放锁。比如 线程 1 要先锁定A, 然后再锁定B; 线程2要锁定B, 然后再锁定A,两个线程同时启动,就会出现死锁了。下面是一个例子,创建一个朋友类,当朋友向我们鞠躬的时候,我们也要向朋友鞠躬,这样才算一个完整的动作。当两人同时鞠躬的时候,都在等待对方鞠躬。这时就造成了死锁。

/**
 * 死锁模拟程序
 */
public class Deadlock {
    /**
     * 朋友实体类
     */
    static class Friend {
        // 朋友名字
        private final String name;

        // 朋友实体类型的构造方法
        public Friend(String name) {
            this.name = name;
        }

        // 获取名字
        public String getName() {
            return this.name;
        }

        // 朋友向我鞠躬方法,(同步的)
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s" + "  has bowed to me!%n", this.name, bower.getName());
            bower.bowBack(this);
        }

        // 我回敬鞠躬方法,(同步的)
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s" + " has bowed back to me!%n", this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        // 死锁模拟程序测试开始
        // 创建两个友人alphonse,Gaston
        final Friend alphonse = new Friend("Alphonse");
        final Friend gaston = new Friend("Gaston");
        // 启动两位友人鞠躬的线程。
        new Thread(new Runnable() {
            public void run() {
                alphonse.bow(gaston);
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                gaston.bow(alphonse);
            }
        }).start();
    }
}

参考:

java 线程死锁模拟

volatile 关键字

volatile 关键字,使一个变量在多个线程间可见。

A B线程都用到一个变量running,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道。
使用volatile关键字,会让所有线程都会读到变量的修改值。

在下面的代码中,running是存在于堆内存的t对象中。当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy的变量,并不会每次都去读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行。使用volatile,每当有一个线程发生对工作区变量的赋值操作,其他线程通过cpu总线嗅探机制,将原来自己工作区的变量失效,然后去主内存(堆内存)中读取新的值。

public class T {
    /*volatile*/ boolean running = true;

    void m() {
        System.out.println("m start");
        while (running) {
            /*
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
        }
        System.out.println("m end!");
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t.running = false;
    }
}

volatile和synchronized的区别

synchronized可以保证可见性和原子性,volatile只能保证可见性。

public class T {
    volatile int count = 0;

    /*synchronized*/ void m() {
        for (int i = 0; i < 10000; i++)
            count++;
    }

    public static void main(String[] args) {
        T t = new T();

        List<Thread> threads = new ArrayList<Thread>();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }

        threads.forEach((o) -> o.start());

        threads.forEach((o) -> {
            try {
                o.join();
                /*
                 * t.join()方法阻塞调用此方法的线程(calling thread),直到线程t完成,此线程再继续;
                 * 通常用于在main()主线程内,等待其它线程完成再结束main()主线程
                 */
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(t.count);

    }

}

上面程序的结果,不是理想的10000,因为count++不是原子操作,可能存在写入脏数据。

更多volatile和synchronized的区别:可参考下面文章

volatile和synchronized的区别

要解决上面这个程序的问题可以使用原子数据类型AtomXXX类, 它们的increase之类的操作都是原子操作,它比加synchronized更高效。但是应该注意,执行AtomXXX类的多个方法不构成原子性,虽然它的每个方法都是原子操作,但是在执行完AtomXXX类的上个方法到开始执行AtomXXX类下个方法的这个过程,是有可能被其他线程插入的。

public class T {
    /*volatile*/
    // int count = 0;

    AtomicInteger count = new AtomicInteger(0);

    void m() {
        for (int i = 0; i < 10000; i++)
            // if count.get() < 1000
            count.incrementAndGet(); // 替代count++,count++不是原子操作
    }

    public static void main(String[] args) {
        T t = new T();

        List<Thread> threads = new ArrayList<Thread>();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }

        threads.forEach((o) -> o.start());

        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(t.count);

    }

}

synchronized的优化

同步代码块中的语句越少越好,采用细粒度的锁,可以使线程争用时间变短,从而提高效率。

public class T {

    int count = 0;

    synchronized void m1() {
        // do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
        count++;

        // do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    void m2() {
        // do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
        // 采用细粒度的锁,可以使线程争用时间变短,从而提高效率
        synchronized (this) {
            count++;
        }
        // do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

应该避免将锁定对象的引用变成另外的对象

锁定某对象o,如果o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变。

public class T {

    Object o = new Object();

    void m() {
        synchronized (o) {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());

            }
        }
    }

    public static void main(String[] args) {
        T t = new T();
        // 启动第一个线程
        new Thread(t::m, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 创建第二个线程
        Thread t2 = new Thread(t::m, "t2");

        t.o = new Object(); // 锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会

        t2.start();

    }

}

不要以字符串常量作为锁定对象

在下面的例子中,m1和m2其实锁定的是同一个对象。这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,因为你的程序和你用到的类库不经意间使用了同一把锁。

public class T {

    String s1 = "Hello";
    String s2 = "Hello";

    void m1() {
        synchronized (s1) {

        }
    }

    void m2() {
        synchronized (s2) {

        }
    }

}

sleep()方法 和 wait()方法有什么区别?

sleep()是线程类(Thread)的方法,导致此线程暂停执行指定时间,将执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait()是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

你可能感兴趣的:(Java)