探究Java并发编程(持续更新)

我发现面试的时候经常会问进程和线程的区别。

在理解这个问题上,要先了解计算机操作系统的发展历史和分类。
1.最早的操作系统是手工批处理,依靠人来完成资源分配,这类操作系统效率低,早已淘汰。

2.随后提出了单道批处理系统,由系统自动完成一个或多个用户的作业,不需要手工操作,但是由于I/O远慢于CPU,在等待I/O上浪费了CPU资源,现在也不常见,甚至没有了。

3.之后对于I/O浪费CPU时间的问题,提出了多道程序系统,而为了使得计算机既能够使用CPU又能够使用I/O,麻省理工学院提出了进程的概念。
(1)进程是基于计算机能够充分利用资源提出的概念,在进程控制块(PCB)的控制下,使得计算机根据一定的进程调度算法执行多个程序。
常见的调度算法有:优先调度算法,轮转时间片算法,先来先服务算法等。
(2) 在20年后,由于多个进程的创建、撤消与切换时空开销太大,需要提出轻量级线程,加上当时出现了对称多处理机(拥有更多的处理器),能够运行多个单位。
因此也就提出了线程。在我看来,线程是为了程序并发执行提出的,它所针对的单位是程序,而不是计算机。
在面向对象的编程中,可以这么理解,进程是一个类,而线程是一个对象,在以前需要并发执行程序需要创建两个一样的类,而在线程提出后,只需要创建两个对象即可,我认为单纯认为线程的提出是为了资源利用是不够的,线程的提出产生了新的编程思想,也就是并发编程

4.分时系统,分时系统是我们最常见的系统,比如linux,安卓,ios等,分时系统把处理机的运行时间分成很短的时间片,按时间片轮流把处理机分配给各联机作业使用,而在人眼看来好像多个程序同时执行一样。

5.实时系统,为了满足实时响应提出来的系统,如火箭,导弹发射系统等。

并发编程

本文学习JAVA多线程基础,并加增加底层补充。

基础部分

创建

首先理解一下用户级线程和内核级线程(轻量级进程)的区别:

用户级线程是程序在用户空间中创建的,而内核级线程是操作系统内核创建的。
用户线程的创建、销毁、切换在用户空间中完成且效率高,由于没有系统时间片,用户线程的不能中断切换,当一个线程放弃资源时,下一个线程才能占用,线程的所有调度都是由程序员决定。并且用户级线程拥有更大的空间。不过当进程中的一个线程阻塞时,会阻塞该进程中的其他所有线程。

内核级线程有内核调度,即使阻塞也可以运行其他线程(包括其他进程的线程),缺点就是开销大。

而Java中,java.lang.Thread的实现是:
private static native void registerNatives();
是根据不同平台决定的,是由操作系统决定的。
所以具体java创建的线程是哪种,要根据操作系统来决定,比如Linux和Windows中就是创建内核级线程。

JAVA的程序入口地址是main方法,而main则是程序的主线程。
在JAVA中一定从主线程main开始执行。

在JAVA中线程类的创建有两种方法:
(1)继承java.lang.Thread
(2)实现java.lang.Runnable接口

继承java.lang.Thread和实现java.lang.Runnable接口的区别
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

java中的Runnable接口只有run方法。
而想要通过实现Runnable接口去创建线程,则需要new 一个Thread对象将Runnable接口的实现类传递进去,再通过Thread来创建线程。
本质上没有区别,是操作上的区别。
对于多个线程执行不同任务时,选择继承java.lang.Thread的方式。
对于多个线程执行同样的人物时,选择实现java.lang.Runnable接口。

start()和run()
public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

这是start的实现代码。

其中start0的方法为:private native void start0();由平台决定。

而在Thread中定义了:

private static native void registerNatives();
    static {
        registerNatives();
    }

Thread 类有个 registerNatives 本地方法,该方法主要的作用就是注册一些本地方法供 Thread 类使用,如 start0(),stop0() 等等,可以说,所有操作本地线程的本地方法都是由它注册的。
这个方法放在一个 static 语句块中,当该类被加载到 JVM 中的时候,它就会被调用,进而注册相应的本地方法。
而本地方法 registerNatives 是定义在 Thread.c 文件中的。Thread.c 是个很小的文件,它定义了各个操作系统平台都要用到的关于线程的公用数据和操作。

而run()方法:

 public void run() {
        if (target != null) {
            target.run();
        }
    }

其中target在java.lang.Thread中定义:private Runnable target;

private Thread(ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals)
                   {this.target = target;}

所以实际上调用run()方法并没有创建一个线程,而是直接当前调用了该线程的方法。
而start()则是创建了一个线程,等获得资源后,再调用run()方法。

暂停

调用java.lang.Thread的类方法sleep。
public static native void sleep(long millis) throws InterruptedException;
单位是ms,通过调用jvm来暂停。
值得注意的是,线程被暂停时不会交付所持锁。

互斥

1.synchronized方法:
2.synchronized声明:
对于锁的研究会在后面深入研究

中断

java.lang.Thread类有一个interrupt方法,该方法只是会设置线程的中断标志位,没有任何其它作用。
而中断的处理需要程序员自己处理。
interrupt方法
Thread实例方法:必须由其它线程获取被调用线程的实例后,进行调用。实际上,只是改变了被调用线程的内部中断状态;
Thread.interrupted方法
Thread类方法:必须在当前执行线程内调用,该方法返回当前线程的内部中断状态,然后清除中断状态(置为false) ;
isInterrupted方法
Thread实例方法:用来检查指定线程的中断状态。当线程为中断状态时,会返回true;否则返回false。

等待和唤醒

wait()等待,是调用object的wait方法。
而线程被唤醒的条件为:
1.有其它线程以notify方法唤醒该线程(唤醒一个)
2.有其它线程以notifyAll方法唤醒该线程(唤醒全部)
3.有其它线程以interrupt方法唤醒该线程(修改中断状态)
4.wait方法已到期
在这里插入图片描述

java内存模型

说起内存模型,先说内存结构。
java的运行:
1.首先是执行javac,将.java转成.class字节码。
2.jvm的类加载器再将各个类的.class文件加载
3.jvm的执行引擎从main开始执行程序

在程序的执行过程中,所用到的空间叫做Runtime Data Area(运行时数据区),也就是jvm内存。

在这里插入图片描述
而内存模型指的是:
主存储器与工作存储器
主存储器(Main Memory):类的实例所存在的区域,main memory为所有的线程所共享。
工作存储器(Working Memory):每个线程各自独立所拥有的作业区,在working memory中,存有main memory中的部分拷贝,称之为工作拷贝(working copy)
在这里插入图片描述
当线程需要引用主内存的共享变量时将变量拷贝到工作内存中。
当线程需要赋值时,会将值赋值给共享变量副本,再由JVM将共享变量副本拷贝到共享变量中。

Java原子操作:
1.read:负责从主存储器(main memory)拷贝到工作存储器(working memory)
2.write:与上述相反,负责从工作存储器(working memory)拷贝到主存储器(main memory)
3.use:表示线程引用工作存储器(working memory)的值
4.assign:表示线程将值指定给工作存储器(working memory)
5.lock:表示线程取得锁定
6.unlock:表示线程解除锁定

synchronized:线程同步
volatile:内存同步

synchronized一般有两类操作,读和写。
当线程进入synchronized时:
1.读:将自己的工作内存释放,引用主内存。(lock->read)
2.写:将工作内存拷贝到主内存。(write->unlock)
当线程取消synchronized时会强制执行写

volatile:内存同步,read;

临界区

Single Threaded Execution 是指“以1个线程执行”的意思,有时也称为Critical Section(临界区)。

对于临界区资源,也就是共享资源,如果不加锁任意访问将会出现严重的错误。
上面说到,每个工作区都有自己的副本,每次写不是直接对共享变量操作,而是先把主内存的共享变量拷贝到工作区的副本中,再执行操作后,将自己的副本拷贝到共享区中。
此时如果两个线程同时做累加工作,由于不是直接对共享做加法而是先读,再加,再写。
所有当线程a读了1之后,线程b写入2,此时线程a加1后把2写入主内存,此时有一次累加就被覆盖了。
对于线程a,b同时累加1000000次后,正常结果应该是2000000,但由于上述原因结果会小于2000000

见代码:

public class car {

	public int num=0;
	public car() {
		// TODO Auto-generated constructor stub
	}
	public void addcar()
	{
		for(int i=0;i<1000000;i++)
		{
			num++;
		}
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		car c=new car();
		TH th1=new TH(c);
		TH th2=new TH(c);
		th1.start();
		th2.start();
		try {
			th1.join();
			th2.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(c.num);
	}

}

说明一下join方法是让正在执行的线程等待join方法的线程执行完,再执行

public class TH extends Thread {

	car c;
	public void run()
	{
			c.addcar();
	}
	public TH(car c) {
		// TODO Auto-generated constructor stub
		this.c=c;
	}

}

正确结果应该是:2000000
但实际结果:
第一次:1544665
第二次:1732061
第三次:1861078
每一次都不一样。

所以在访问临界资源时,一定要加锁,保证只有一个线程进入临界区。
锁的实现:

synchronized

方法一

public synchronized void method()
{
   // todo
}

将锁加在方法前面,整个方法只能由一个线程执行,当该线程执行时其他线程需要等待该线程执行完后释放锁
例如

package test_Thread;

public class car {

	public int num=0;
	public car() {
		// TODO Auto-generated constructor stub
	}
	public synchronized void addcar()
	{
		for(int i=0;i<1000000;i++)
		{
			num++;
		}
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		car c=new car();
		TH th1=new TH(c);
		TH th2=new TH(c);
		th1.start();
		th2.start();
		try {
			th1.join();
			th2.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(c.num);
	}

}

将上面的代码加锁后,每次执行结果都是2000000

方法二

public void method()
{
   synchronized(this) {
      // todo
   }
}

目的是保护一个代码块为临界区,每次访问时,都要获得锁和等待锁,其中传递的参数是对象,是在这个代码块中保护该对象。

public void addcar()
	{
		synchronized(this) {
		for(int i=0;i<1000000;i++)
		{
			num++;
		}
		}
	}

上述代码改成这样,结果同样正确。

同样上述代码可以保护对象为自己,也可以保护的对象为其他对象。

public void method3(SomeObject obj)
{
   //obj 锁定的对象
   synchronized(obj)
   {
      // todo
   }
}

例如:

package test_Thread;

public class car {

	public int[] n=new int[1];
	public car() {
		// TODO Auto-generated constructor stub
		n[0]=0;
	}
	public void addcar()
	{
		synchronized(n) {
		for(int i=0;i<1000000;i++)
		{
			n[0]++;
		}
		}
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		car c=new car();
		TH th1=new TH(c);
		TH th2=new TH(c);
		th1.start();
		th2.start();
		try {
			th1.join();
			th2.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(c.n[0]);
	}

}

当然也可以随意置一个变量保护区域

例如:

public class car {
	int num=0;
	String clock="clock";
	public void addcar()
	{
		synchronized(clock) {
		for(int i=0;i<1000000;i++)
		{
			num++;
		}
		}
	}
}

最终结果依然正确

方法三:
修饰一个静态方法

public synchronized static void method() {
   // todo
}

静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象

方法四:
修饰一个类

class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo
      }
   }
}

无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

我发现了一个有趣的现象

public synchronized void addcar()
	{
		for(int i=0;i<1000000;i++)
		{
			num++;
		}
	}
	public void addcar2()
	{
		for(int i=0;i<1000000;i++)
		{
			num++;
		}
	}

此时线程1执行方法addcar,线程2执行方法addcar2。
这个时候num的值不对。
所以我怀疑synchronize是逻辑实现,学过操作系统的都知道访问临界区资源时,首选判断信号量是否大于等于1,而信号量是内核提供的共享变量,使用硬件方法保护。
通过信号量去保护临界区。

synchronize的底层实现也类似,但是更加优化,以后去学JVM的时候再研究。
先放一个链接synchronized底层原理

Lock

对于上述问题也可以通过Lock来解决
例如:

Lock lock=new ReentrantLock
public void addcar()
	{
		lock.lock();
		try {
	       	for(int i=0;i<1000000;i++)
		     {
			     num++;
		    }    
		}
		finally {
			lock.unlock();
		}
	}

Lock和synchronized有很大的区别,需要仔细研究。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值