进程、线程、多线程、并发与并行、线程运行内存模型、锁

进程、线程、多线程、并发与并行、线程运行内存模型、锁

1.1我们为什么需要多线程?
比如我们在听歌时可以一边听一边下载,这种一个程序看起来同时做好几件事的情况,就需要多线程。

线程(Thread)和进程(Process)的关系很紧密,进程和线程是两个不同的概念,进程的范围大于线程。通俗地说,进程就是一个程序,线程是这个程序能够同时做的各件事情。比如,媒体播放机运行时就是一个进程,而媒体播放机同时做的下载文件和播放歌曲,就是两个线程。因此,可以说进程包含线程。

线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所有的全部资源。一个进程中是可以拥有很多线程的。

1.2实现多线程的方法

实现多线程有很多方法:Thread 继承 、Runnable 接口实现 、Callable 接口实现(带返回值) 、线程池(提供线程)。
我们要讲的就是其中的两种: Thread 继承 、Runnable 接口实现

1.2.1继承Thread 类开发多线程

我们可以用多线程来解决“程序看起来同时做好几件事情”的效果,只需要将各个文件传送的工作分别写在线程里即可。

继承 Thread 类实现多线程,该方法步骤如下:
、编写一个类,继承 java.lang.Thread
、在这个类中,重写 java.lang.Thread 类中的以下函数:
public void run()

class FileTransThread extends Thread{
      private String fileName;
      public FileTransThread(String fileName){ 
          this.fileName = fileName;
      }
      public void run(){
          System.out.println("传送" + fileName); 
             try{
                Thread.sleep(1000 * 10);
                }
                catch(Exception ex){}
                System.out.println(fileName + "传送完毕");
      }
 }

线程编写完毕

、实例化线程对象,调用其 **start()**函数来启动该线程。

FileTransThread ft1 = new FileTransThread(“文件 1);
ft1.start();

1.2.2实现Runnable 接口开发多线程

该方法实现步骤如下:
、编写一个类,实现 java.lang.Runnable 接口
、在这个类中,重写 java.lang. Runnable 接口中的以下函数:
public void run()
将线程需要执行的代码放入run方法。

class FileTransRunnable implements Runnable{ 
      private String fileName ;
      public FileTransRunnable(String fileName){ 
          this.fileName = fileName ;
      }
      public void run(){
          System.out.println("传送" + fileName); 
          try{
               Thread.sleep(1000 * 10);
             }catch(Exception ex){}
          System.out.println(fileName + "传送完毕");
      }
}

、实例化 java.lang.Thread 对象,实例化上面编写的 Runnable 实现类,将后者传入 Thread 对象的构造函数。调用 Thread 对象的 start方法来启动线程。

FileTransRunnable fr1 = new FileTransRunnable("文件 1");
Thread th1 = new Thread(fr1);
th1.start();

1.2.3两种方法的不同之处

、继承 Thread 的方法,具有如下特点:

每一个对象都是一个线程,其对象具有自己的成员变量。比如:

class FileTransThread extends Thread{ private String fileName;
public void run(){
}
FileTransThread ft1 = new FileTransThread(); 
FileTransThread ft2 = new FileTransThread();

此时,线程 ft1ft2 具有各自的 fileName 成员变量,除非将 fileName 定义为静态变量。

Java 不支持多重继承,继承了 Thread,就不能继承其他类。因此,该类主要完成线程工作,功能比较单一。

二、实现 Runnable 的方法,具有如下特点:

每一个对象不是一个线程,必须将其传入 Thread 对象才能运行,各个线程是否共享 Runnable 对象成员,视情况而定。比如有如下 Runnable 类:

class FileTransRunnable extends Runnable{ 
      private String fileName ;
      public void run(){
      }
}

下面代码:

FileTransRunnable fr1 = new FileTransRunnable(); 
FileTransRunnable fr2 = new FileTransRunnable(); 
Thread th1 = new Thread(fr1);
Thread th2 = new Thread(fr2);

此时,线程 th1th2 访问各自的 fileName 成员变量,因为它们传进来的FileTransRunnable 是不同的。但是如下代码:

FileTransRunnable fr = new FileTransRunnable();
Thread th1 = new Thread(fr);
Thread th2 = new Thread(fr);

线程 th1th2 访问的确实同一个 fileName 成员变量,因为它们传进来的FileTransRunnable 是同一个对象。

Java 不支持多重继承,却可以支持实现多个接口,因此,我们有时可以给一些继承了某些父类的类,通过实现 Runnable 的方法增加线程功能。

1.3并发和并行

并发是指两个或多个事件在同一时间间隔内发生。操作系统的并发性是指计算机系统中同时存在多个运行的程序,因此它具有处理和调度多个程序同时执行的能力。在操作系统的目的是使程序能并发执行。

并发是指同一时间间隔,并行是指同一时刻。在多线程环境下,一段时间内宏观上有多个线程在同时执行,而在每个时刻,单处理机环境下实际仅能有一道程序执行,因此微观上这些线程仍是分时交替执行的。操作系统的并发性是通过分时得以实现的。并行性是指系统具有同时进行运算或操作的特性,在同一时刻能完成两种或两种以上的工作。并行需要相关硬件支持,多个CPU

2.线程运行的内存模型和硬件架构

2.1线程运行的内存架构

每一个运行在Java虚拟机里的线程都拥有自己的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问自己的线程栈。一个线程创建的本地变量对其它线程不可见,仅自己可见。即使两个线程执行同样的代码,这两个线程任然在在自己的线程栈中的代码来创建本地变量。因此,每个线程拥有每个本地变量的独有版本。

所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。

堆上包含在Java程序中创建的所有对象,无论是哪一个对象创建的。这包括原始类型的对象版本。如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,这个对象任然是存放在堆上。

一个本地变量可能是原始类型,在这种情况下,它总是“呆在”线程栈上。一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量任然存放在线程栈上,即使这些方法所属的对象存放在堆上。一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。静态成员变量跟随着类定义一起也存放在堆上。

存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个本地变量的私有拷贝。

下图演示了调用栈和本地变量存放在线程栈上,对象存放在堆上。
在这里插入图片描述
两个线程拥有一些列的本地变量。其中一个本地变量(本地变量2)执行堆上的一个共享对象(对象3)。这两个线程分别拥有同一个对象的不同引用。这些引用都是本地变量,因此存放在各自线程的线程栈上。这两个不同的引用指向堆上同一个对象。

注意,这个共享对象(对象3)持有对象2和对象4一个引用作为其成员变量(如图中对象3指向对象2和对象4的箭头)。通过在对象3中这些成员变量引用,这两个线程就可以访问对象2和对象4。

这张图也展示了指向堆上两个不同对象的一个本地变量。在这种情况下,指向两个不同对象的引用不是同一个对象。理论上,两个线程都可以访问对象1和对象5,如果两个线程都拥有两个对象的引用。但是在上图中,每一个线程仅有一个引用指向两个对象其中之一。

2.2硬件内存架构
下图是硬件架构的简单图示:
在这里插入图片描述
每个CPU都包含一系列的寄存器,它们是CPU内内存的基础。CPU在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU访问寄存器的速度远大于主存。

每个CPU可能还有一个CPU缓存层。实际上,绝大多数的现代CPU都有一定大小的缓存层。CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还要慢一点。一些CPU还有多层缓存,但这些对理解Java内存模型如何和内存交互不是那么重要。只要知道CPU中可以有一个缓存层就可以了。

一个计算机还包含一个主存。所有的CPU都可以访问主存。主存通常比CPU中的缓存大得多。

3.锁

3.1为什么需要锁

这涉及到了线程同步(synchronize)问题。我们通过一个例子来了解一下这个现象。

有若干张飞机票,2 个线程去卖它们,要求没有票时能够提示: 没有票了。以最后剩下 3 张票为例。

class TicketRunnable implements Runnable{ 
      private int ticketNum = 3;	//以 3 张票为例
      public void run(){
             while(true){
                   String tName = Thread.currentThread().getName();
                   if(ticketNum<=0){
                           System.out.println(tName + “无票”);
                           break;
                        }else{
                            try{
                                Thread.sleep(1000);//程序休眠 1000 毫秒
                               }catch(Exception ex){} 
              ticketNum--;
              System.out.println(tName + “卖出一张票,还剩” + ticketNum + “张票”);
                     }
           }
      }
 }
 
public class ThreadSynTest2 {
          public static void main(String[] args){ 
          TicketRunnable tr = new TicketRunnable(); 
          Thread th1 = new Thread(tr,” 线 程 1); 
          Thread th2 = new Thread(tr,” 线 程 2); th1.start();
          th2.start();
      }
 }

运行结果如下:
在这里插入图片描述
一张票被卖出两次,甚至出现负数。显然结果不可靠。这时候我们就需要锁。

3.2如何解决上述问题

怎样解决这个问题?很简单,就是让一个线程卖票时其他线程不能抢占 CPU。根据定义,实际上相当于要实现线程的同步,通俗地讲,可以给共享资源(在本例中为票)加一把锁,这把锁只有一把钥匙。哪个线程获取了这把钥匙,才有权利访问该共享资源。
有一种比较直观的方法,可以在共享资源(如“票”)每一个对象内部都增加一个新成员,标识“票”是否正在被卖中,其他线程访问时,必须检查这个标识,如果这个标识确定票正在被卖中,线程不能抢占 CPU。这种设计理论上当然也是可行,但由于线程同步的情况并不是很普遍,仅仅为了这种小概率事件,在所有对象内部都开辟另一个成员空间,带来极大的空间浪费,增加了编程难度,所以,一般不采用这种方法。现代的编程语言的设计思路都是把同步标识加在代码段上,确切的说,是把同步标识放在“访问共享资源(如卖票)的代码段”上。
在 Java 语言中,synchronized 关键字可以解决这个问题,整个语法形式表现为:

synchronized(同步锁对象) {
// 访问共享资源,需要同步的代码段
 	}	

注意,synchronized 后的“同步锁对象”,必须是可以被各个线程共享的,如 this、某个全局标量等。不能是一个局部变量。
其原理为:当某一线程运行同步代码段时,在“同步锁对象”上置一标记,运行完这段代码,标记消除。其他线程要想抢占 CPU 运行这段代码,必须在“同步锁对象” 上先检查该标记,只有标记处于消除状态,才能抢占 CPU。在上面的例子中,this 是一个“同步锁对象”。
因此,在上面的案例中,可以将将卖票的代码用 synchronized 代码块包围起来,
“同步锁对象”取 this。代码如下:

class TicketRunnable implements Runnable { 
      private int ticketNum = 3; // 以 3 张票为例
      public void run() {
          while (true) {
              String tName = Thread.currentThread().getName();
             // 将需要独占 CPU 的代码用 synchronized(this)包围起来
              synchronized (this) {
                 if (ticketNum <= 0) { 
                   System.out.println(tName + “票”); break;
                 } else {
                       try {
                           Thread.sleep(1000);// 程序休眠 1000 毫秒
                      } catch (Exception ex) {}
               ticketNum--; 
               System.out.println(tName + “卖出一张票,还剩” +ticketNum “张票”);
               }
            }
        }
      }
}

public class ThreadSynTest3 {
    public static void main(String[] args) { 
        TicketRunnable tr = new TicketRunnable(); 
        Thread th1 = new Thread(tr, “ 线 程 1);
        Thread th2 = new Thread(tr, “线程 2);
        th1.start();
        th2.start();
    }
}

运行结果如下:
在这里插入图片描述
从以上代码可以看出,该方法的本质是将需要独占CPU 的代码用**synchronized(this)**包围起来。如前所述,一个线程进入这段代码之后,就在 this 上加了一个标记,直到该线程将这段代码运行完毕,才释放这个标记。如果其他线程想要抢占 CPU,先要检查 this 上是否有这个标记。若有,就必须等待。
实际上,在 Java 内,还可以直接把 synchronized 关键字直接加在函数的定义上, 这也是一种可以推荐的方法。如:

public synchronized void f1() {
// f1 代码段
}

效果等价于:

public void f1()	{ 
    synchronized(this){
// f1 代码段
    }
}

3.3Lock类

我们也可以使用Java中的Lock类,使用方法如下:

public void number(){
		//添加锁
		lock.lock();
		//原子操作   volatile
		data.count++;
		System.out.println(Thread.currentThread().getName()+": "+data.count);
	
		//释放锁
		lock.unlock();
	}

将需要独占资源的代码写在lock方法与unlock方法之间。
原子操作:原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会切换到其他线程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值