Java多线程学习(一)
一.进程与线程
进程:是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
线程:是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的 资源。
虽然系统是把资源分给进程,但是CPU很特殊,是被分配到线程的,所以线程是CPU分配的基本单位。
二者关系:
一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
程序计数器:是一块内存区域,用来记录线程当前要执行的指令地址(代码区地址) 。
栈:用于存储该线程的局部变量,这些局部变量是该线程私有的,除此之外还用来存放线程的调用栈祯(方法的调用地址栈)。
堆:是一个进程中最大的一块内存,堆是被进程中的所有线程共享的(也就是代码中new出的对象)。
方法区:则用来存放 NM 加载的类、常量及静态变量等信息,也是线程共享的 。
二者区别:
进程:有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。
线程:是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。
-
简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
-
线程的划分尺度小于进程,使得多线程程序的并发性高。
-
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
-
每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
-
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别
二.并发与并行
并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。
并行:是说在单位时间内多个任务同时在执行 。
在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。
并发过程中常见的问题:
1、线程安全问题:
多个线程同时操作共享变量1时,会出现线程1更新共享变量1的值,但是其他线程获取到的是共享变量没有被更新之前的值。就会导致数据不准确问题。
2、共享内存不可见性问题
Java内存模型(处理共享变量)
Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量 。(如上图所示)
(实际工作的java内存模型)
上图中所示是一个双核 CPU 系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。CPU的每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。 那么Java内存模型里面的工作内存,就对应这里的 Ll或者 L2 缓存或者 CPU 的寄存器
1、线程A首先获取共享变量X的值,由于两级Cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0的值缓存到两级缓存,线程A修改X的值为1,然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache内和主内存里面的X的值都是l。
2、线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X=1;到这里一切都是正常的,因为这时候主内存中也是X=l。然后线程B修改X的值为2,并将其存放到线程2所在的一级Cache和共享二级Cache中,最后更新主内存中X的值为2,到这里一切都是好的。
3、线程A这次又需要修改X的值,获取时一级缓存命中,并且X=l这里问题就出现了,明明线程B已经把X的值修改为2,为何线程A获取的还是l呢?这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见。
synchronized 的内存语义:
这个内存语义就可以解决共享变量内存可见性问题。进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。会造成上下文切换的开销,独占锁,降低并发性
Volatile的理解:
该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时-,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。volatile的内存语义和synchronized有相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。不能保证原子性
这里牵涉两个关键字synchronized和Volatile可能初学者不熟悉 下面我们进行java锁机制的介绍。
三.java锁机制
请先浏览下面的文章
多线程的锁机制
这里主要把文章重点进行分析
一.单例模式设计
java很多时候避免产生资源空间的浪费使用单例模式设计。
eg:
public class A{
private A a;
public static A getInstance(){
if (a == null) { a = new A(); }
return a;};
}
这样在多线程设计时可能会出现多个线程同时访问getInstance()方法.这样做有什么问题呢?
原因是在方法内进行了空值判断,如为空值则新申请一个类。这样多个线程可能同时申请导致申请了不同的类,文章中打印出申请各类的hashcode如下:
我们发现线程7申请的类与其他类不同,说明单例写法存在问题。
另外使用for循环依次开启线程name不递增问题,Debug调试的时候输出就是0到N的顺序,不用Debug调试直接运行就不是0到N输出。因为0-N这段循环时间对CPU来说太短,对于CPU分配给这个线程的时间片来说足够完成了,大致可以理解为创建了所有的线程,再争用资源。调试的时候有断点,产生了优先级,所以才按序输出的。
二.线程重入(同步)问题
这个问题个人觉得文章讲的不是很典型,文章显示的大概率是对线程同步问题的分析。也就是多个线程同时执行,先后顺序的控制问题。
我们用Synchronized关键字锁住方法后 ,两个线程分别调用同一对象的不同Synchronized修饰的方法
为帮助读者更好理解文章
将不用标志位控制代码(仅加入Synchronized)给出对比
package my;
//每次生产100件产品,每次消费20件产品,生产消费更替12轮
public class s
{
public static void main(String[] args)
{
final FactoryCopy factory = new FactoryCopy();
new Thread(new Runnable()
{
@Override
public void run()
{
try
{
Thread.sleep(2000);
}
catch (InterruptedException e)
{ e.printStackTrace();
}
for (int i = 1; i <= 12; i++)
{
factory.createProduct(i);
}
}
}).start();
new Thread(new Runnable()
{
@Override
public void run()
{
try
{
Thread.sleep(2000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
for (int i = 1; i <= 12; i++)
{
factory.sellProduct(i);
}
}
}).start();
}
}
class FactoryCopy
{
//生产产品
public void createProduct(int i)
{
for (int j = 1; j <= 100; j++)
{
System.out.println("第" + i + "轮生产,产出" + j + "件");
}
}
//销售产品
public void sellProduct(int i)
{
for (int j = 1; j <= 20; j++)
{
System.out.println("第" + i + "轮销售,销售" + j + "件");
}
}
}
以上为文章源码 本人重新运行后结果为
我们可以看到生产和销售线程同时打印
我们可以看出加入Synchronized关键字后 实现了线程A的单方面运行而线程B需等待A线程生产方法结束才能运行。
看完可能会有疑问,Synchronized关键字是指锁定了方法只能被单一线程访问,但是例子中两个线程分别调用的是同一类不同的被锁定方法,为什么不能同时调用呢。下面我们讲讲Synchronized关键字的机制。
三.Synchronized关键字
参考文章:
java锁机制了解一下
Synchronized关键字详解
- synchronized是Java的一个关键字,它能够将代码块(方法)锁起来
它使用起来是非常简单的,只要在代码块(方法)添加关键字synchronized,即可以实现同步的功能
public synchronized void test() {
// doSomething
}
- synchronized是一种互斥锁
一次只能允许一个线程进入被锁住的代码块
- synchronized是一种内置锁/监视器锁
Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用对象的内置锁(监视器)来将代码块(方法)锁定的!
看到这你可能有点懵,别急,接着看下面
synchronized如何使用
一般 我们用它来修饰三样东西
- 修饰普通方法
- 修饰代码块
- 修饰静态方法
修饰普通方法
public class Java3y {
// 修饰普通方法,此时用的锁是Java3y对象本身(内置锁) 也就是this指针
public synchronized void test() {
// doSomething
}
}
修饰代码块
public class Java3y {
public void test() {
// 修饰代码块,此时用的锁是Java3y对象(内置锁)--->this
synchronized (this){
// doSomething
}
}
}
修饰静态方法
由于上面所说任何对象都有内置锁,那么我们这里不使用this,可以这么做
public class Java3y {
// 使用object作为锁(任何对象都有对应的锁标记,object也不例外)
private Object object = new Object();
public void test() {
// 修饰代码块,此时用的锁是自己创建的锁Object
synchronized (object){
// doSomething
}
}
}
上面那种方式(随便使用一个对象作为锁)在书上称之为–>客户端锁,这是不建议使用的。
当然为什么不建议,博主还没有仔细研究,这里留给读者自行研究上面的参考文章。
我们可以看到用synchronized修饰代码块和方法都是使用的对象本身的内置锁this,那么实际上这两种方式本质是相同的不同点在于修饰代码块需要显示申明锁,修饰方法则是隐式申明锁,为下文讲重入锁有帮助。
类锁与对象锁
synchronized修饰静态方法获取的是类锁(类的字节码文件对象),synchronized修饰普通方法或代码块获取的是对象锁。
这里与文章基本一致,很好理解直接上代码。
public class SynchoronizedDemo {
//synchronized修饰非静态方法
public synchronized void function() throws InterruptedException {
for (int i = 0; i <3; i++) {
Thread.sleep(1000);
System.out.println("function running...");
}
}
//synchronized修饰静态方法
public static synchronized void staticFunction()
throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
System.out.println("Static function running...");
}
}
public static void main(String[] args) {
final SynchoronizedDemo demo = new SynchoronizedDemo();
// 创建线程执行静态方法
Thread t1 = new Thread(() -> {
try {
staticFunction();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 创建线程执行实例方法
Thread t2 = new Thread(() -> {
try {
demo.function();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 启动
t1.start();
t2.start();
}
}
结果证明:类锁和对象锁是不会冲突的!
重入锁
我们来看下面的代码
public class Widget {
// 锁住了
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
// 锁住了
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
- 当线程A进入到LoggingWidget的doSomething()方法时,此时拿到了LoggingWidget实例对象的锁。
- 随后在方法上又调用了父类Widget的doSomething()方法,它又是被synchronized修饰。
- 那现在我们LoggingWidget实例对象的锁还没有释放,进入父类Widget的doSomething()方法还需要一把锁吗?
不需要的!
因为锁的持有者是“线程”,而不是“调用”。线程A已经是有了LoggingWidget实例对象的锁了,当再需要的时候可以继续“开锁”进去的!
这就是内置锁的可重入性。
到此便很好理解线程的重入,持有相同锁的线程可以轻易的访问被同一把锁修饰的方法或代码块。
以上为这三天学习心得,将会持续更新。
对重入有兴趣可以了解下面文章