一、线程的运行模式

1、线程与进程

为了实现在同一个时间运行多个任务,Java引入了多线程的概念。在Java中可以通通过方便、快捷的方式启动多线程模式。多线程常被应用在符合并发机制的程序中,例如网络程序等。

人体可以同时进行呼吸、血液循环、思考问题等活动,可以边听歌边聊天…这种机制在Java中被称为并发机制,通过并发机制可以实现多个线程并发执行,这样多线程就应运而生了。

以多线程在Windows操作系统中的运行模式为例,Windows操作系统事多任务操作系统,它以进程为单位。每个独立执行的程序都被称为进程,比如只在运行的QQ、微信、谷歌浏览器等,每一个都是一个进程,每个进程都包含多个线程。系统可以分配给每个进程一段使用CPU的时间,然后CPU在这段时间执行某个进程,进程中的每个线程也被分配到一小段执行时间,这样一个进程就可以具有多个线程并发执行的线程。接下来,下一个CPU时间段又执行另外另一个进程。由于CPU转换的较快,可以使每个进程好像是被同时执行一样。

多线程在Windows操作系统中的运行模式如下:

java 不同进程读写同一个文件_同步方法

  • 进程:单独运行的程序就是一个独立的进程,进程之间是相互独立存在的。比如在Windows系统中运行的QQ、微信、谷歌浏览器程序等,都是一个一个单独的进程。
  • 线程:进程想要执行任务需要依赖线程,进程中的最小执行单位就是线程,并且一个进程中至少有一个线程。
  • 多线程:比如运行谷歌浏览器,浏览器提供很多功能,可以执行下载任务,可以执行播放歌曲任务,也可以执行浏览网页任务等,这些任务就是一个又一个线程。

2、运行模式

(1)串行、并行、并发

一个进程由多个线程构成,一个线程又是由多个任务构成的。线程在执行任务时需要考虑以什么样的模式来执行,是按照顺序逐一运行,还是允许有时间片段交叉或重合的同时运行。这里需要引入两个概念:线程串行、线程并行

线程有两种运行模式:串行和并行。

  • 串行:所谓串行其实是相对于单条线程来执行多个任务来说的,我们就拿下载文件来举个例子,我们下载多个文件,在串行中它是按照一定的顺序去进行下载的,也就是说必须等下载完A之后,才能开始下载B,它们在时间上是不可能发生重叠的
  • 并行:是说物理上的 “同时” 被执行。下载多个文件,开启多条线程,多个文件同时进行下载,这里是严格意义上的在同一时刻发生的,并行在时间上是重叠的。
  • 并发:是一种程序设计,能够让多个任务在逻辑上交织执行。并发设计的程序,可以启动n个线程,比如2个,然后交给2个核,这时两个线程就是并行执行的(“同时”);这两个线程也可以被1个核 “交替” 执行。

很多时候,会认为并行就是真的同时执行,而并发就是交替执行,这是一般的理解,但是并发真正含义是指设计的程序允许同时 或 交替执行,是一种程序设计方案

(2)单CPU中并行与并发的关系

单CPU不同核数情况下,并行与并发的关系总结如下:

  • 单CPU中进程只能是并发,多CPU计算机中进程可以并行。
  • 单CPU单核中线程只能并发,单CPU多核中线程可以并行
  • 无论是并发还是并行,使用者来看,看到的是多进程,多线程。

二、线程安全

在单线程程序中,每次只做一件事,后面的事情需要等待前面的事情完成后才能执行。如果需要用多线程程序,就会发生两个线程抢占资源的问题,例如两个人以相反的方向同时过一个独木桥,这时候就会涉及到多线程编程中的资源抢占问题。

在操作系统中,线程是不拥有资源的,进程是拥有资源的。而线程是由进程创建的,一个进程可以创建多个线程,这些线程共享着进程中的资源。所以,当线程一起并发运行时,同时对一个数据进行修改,就可能会造成数据的不一致性。

在实际开发中,使用多线程程序的情况很多,如银行排号系统、火车站售票系统等。

多线程的程序通常会存在一些安全问题,以火车站售票系统为例:有一段判断当前的票数是否大于0的程序,如果程序判断大于0就执行把火车票售给乘客操作。

现在发生了一种常见的情况:如果当前仅剩下少量的票,多个线程同时访问判断程序后得到当前票数大于0的结果,如果此时访问的线程数大于剩余票数,多个线程又根据判断结果(大于0)都执行了售票操作,那么系统票数就会为负数,明显这种情况是不能被允许的。因此,我们在编写多线程程序时,应该考虑到线程安全问题。

而实际上,线程安全问题大多来源于多个线程同时操作单一对象数据的情况。下面我们看个例子:

实例1:多线程并发操作同一数据造成的不一致问题

在一个周末的晚上,哆啦A梦邀请了胖虎,小静,大雄去帮他卖铜锣烧,他们三位分别摆起了地摊,而这时候哆啦A梦拿出了30个铜锣烧希望他们一起卖完这30个!

分析:为了提高卖出铜锣烧的效率,那么就为他们三个人每人开启一个线程,这样就能很快的卖完!

public class SellThread implements Runnable {
	private int num = 20;
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true) {
			if(num>0){
				try {
					Thread.sleep(300);//卖一会儿休息一会儿
					System.out.println(Thread.currentThread().getName()+"帮多啦A梦卖第"+num--+"个铜锣烧");
				}catch(InterruptedException e) {
					e.printStackTrace();
				}
   	 		}	
		}	
	}
}

public class SellTest {
	public static void main(String[] args) {
		
		SellThread t = new SellThread();
		
		Thread daxiong = new Thread(t,"大雄");
		Thread xiaojing = new Thread(t,"小静");
		Thread panghu = new Thread(t,"胖虎");
		
		daxiong.start();
		xiaojing.start();
		panghu.start();
	}
}
public class SellTest {
	public static void main(String[] args) {
		
		SellThread t = new SellThread();
		
		Thread daxiong = new Thread(t,"大雄");
		Thread xiaojing = new Thread(t,"小静");
		Thread panghu = new Thread(t,"胖虎");
		
		daxiong.start();
		xiaojing.start();
		panghu.start();
	}
}
Console:

大雄帮多啦A梦卖第20个铜锣烧
胖虎帮多啦A梦卖第18个铜锣烧
小静帮多啦A梦卖第19个铜锣烧
大雄帮多啦A梦卖第17个铜锣烧
小静帮多啦A梦卖第15个铜锣烧
胖虎帮多啦A梦卖第16个铜锣烧
大雄帮多啦A梦卖第14个铜锣烧
小静帮多啦A梦卖第13个铜锣烧
胖虎帮多啦A梦卖第12个铜锣烧
大雄帮多啦A梦卖第11个铜锣烧
小静帮多啦A梦卖第10个铜锣烧
胖虎帮多啦A梦卖第9个铜锣烧
胖虎帮多啦A梦卖第7个铜锣烧
大雄帮多啦A梦卖第8个铜锣烧
小静帮多啦A梦卖第8个铜锣烧
胖虎帮多啦A梦卖第6个铜锣烧
大雄帮多啦A梦卖第5个铜锣烧
小静帮多啦A梦卖第4个铜锣烧
胖虎帮多啦A梦卖第3个铜锣烧
小静帮多啦A梦卖第2个铜锣烧
大雄帮多啦A梦卖第1个铜锣烧
小静帮多啦A梦卖第0个铜锣烧
胖虎帮多啦A梦卖第-1个铜锣烧

从输出结果可以看出,不仅卖出了第0个,还有第负个,还有两个人同时卖出同一个的,这明显是不合逻辑的。

这样就是一个并发操作同一个数据造成的线程不安全问题。问题出在这里:

while(true) {
			if(num>0){
				try {
					Thread.sleep(300);//卖一会儿休息一会儿
					System.out.println(Thread.currentThread().getName()+"帮多啦A梦卖第"+num--+"个铜锣烧");
				}catch(InterruptedException e) {
					e.printStackTrace();
				}
   	 	}	
		}

当大雄start了线程,开卖的时候,遇到了sleep方法,就在那里睡了300ms,如果这个时候,胖虎把最后一个铜锣烧给卖掉了,然后大雄醒来后,还继续执行接下来的代码! 这就导致了卖出第0个铜锣烧了!

那么我们要如何解决这个问题呢?

三、线程同步机制(锁机制)

当出现线程安全问题时,我们应该如何解决资源抢占问题呢?这时候我们就需要借助线程同步机制(也称为锁机制)来防治资源冲突。

所有解决线程资源冲突问题的方法都是在指定时间只允许一个线程访问共享资源,这时候就需要给共享资源上一道锁,这就好比一个人上洗手间时,进入洗手间就上锁,防止其他人进入;出来时再将锁打来,然后其他人就可以进去了。

Java的线程同步机制,主要有两方面的内容:

1、同步块

同步机制使用synchronized关键字,使用该关键字的代码块称为同步块,也成临界区。

synchronized(Object){
  //被锁住的代码
}

通常将共享资源的操作放置在synchronized定义的区域内,这样当其他线程获取到这个锁时,就必须等待锁被释放后才能进入该区域。Object为任意一个对象,每个对象都存在一个标识位置,并具有两个值,分别为0和1。

代码块中程序执行逻辑如下:

  • 一个线程运行到同步代码块时首先检查该对象的标识位,如果为0状态,表明此同步块内存在其他线程,这时当前线程处于就绪状态。
  • 直到处于同步代码块中的线程执行玩同步代码块中的代码后,这时该对象的标识位设置为1,当前线程才能开始执行同步代码块中的代码。
  • 执行同步块中的代码的同时,将Object对象的标识位设为0,防止其他线程执行同步代码块中的代码。

实例1中,我们可以使用Java同步代码块来解决线程安全问题:

实例2:使用同步代码块解决线程安全问题

java针对这种情况给我们配了一把锁(同步代码块)!这把锁是来锁一个被共同操作的代码块:

synchronized(new Object){
  private int num = 20;
  //被锁住的代码
  while(true) {
    if(num>0){
      try {
      	Thread.sleep(300);//卖一会儿休息一会儿
      	System.out.println(Thread.currentThread().getName()+"帮多啦A梦卖第"+num--+"个铜锣烧");
    	}
    	catch(InterruptedException e) {
      	e.printStackTrace();
    	}
    }
  }
}

使用同步代码块后,我们的实例1就优化成了这样:

public class SellThread implements Runnable {
	private int num = 25;
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true) {
			synchronized (this) { //加锁
				if(num>0) {
					try {
						Thread.sleep(300);//卖一会儿休息一会儿
					}catch(InterruptedException e) {
						e.printStackTrace();
					}
				System.out.println(Thread.currentThread().getName()+"帮多啦A梦卖第"+num--+"个铜锣烧");
				}
			}	
		}	
	}
}
Console:

大雄帮多啦A梦卖第25个铜锣烧
大雄帮多啦A梦卖第24个铜锣烧
大雄帮多啦A梦卖第23个铜锣烧
大雄帮多啦A梦卖第22个铜锣烧
大雄帮多啦A梦卖第21个铜锣烧
大雄帮多啦A梦卖第20个铜锣烧
大雄帮多啦A梦卖第19个铜锣烧
大雄帮多啦A梦卖第18个铜锣烧
大雄帮多啦A梦卖第17个铜锣烧
大雄帮多啦A梦卖第16个铜锣烧
大雄帮多啦A梦卖第15个铜锣烧
大雄帮多啦A梦卖第14个铜锣烧
大雄帮多啦A梦卖第13个铜锣烧
大雄帮多啦A梦卖第12个铜锣烧
大雄帮多啦A梦卖第11个铜锣烧
大雄帮多啦A梦卖第10个铜锣烧
大雄帮多啦A梦卖第9个铜锣烧
大雄帮多啦A梦卖第8个铜锣烧
大雄帮多啦A梦卖第7个铜锣烧
大雄帮多啦A梦卖第6个铜锣烧
大雄帮多啦A梦卖第5个铜锣烧
大雄帮多啦A梦卖第4个铜锣烧
大雄帮多啦A梦卖第3个铜锣烧
大雄帮多啦A梦卖第2个铜锣烧
大雄帮多啦A梦卖第1个铜锣烧

这样就解决了线程并发的安全问题。不会出现0,-1,重复的情况了。这是因为将共享资源放置在了同步代码块中。

2、同步方法

虽然同步代码块的使用提高了线程的安全性,但是也降低了线程的运行效率!如果出现同步嵌套就容易会出现死锁问题!

除了这样的锁对象之外呢!还有一种是方法锁(同步方法),这种锁是在方法的修饰符后添加synchronized关键字!它的锁对象是this(当前对象!)

同步方法就是被synchronized关键字修饰的方法。

synchronized void func(){
  
}

当某个对象调用了同步方法时,该对象的其他同步方法就必须等待该同步方法执行完毕后才能被执行。必须将每个能访问共享资源的方法都修饰为synchronized,否在会出现错误。

实例3:使用同步方法解决线程安全问题
public class SellThread implements Runnable {
	private int num = 25;
	
	@Override
	public synchronized void run() {
		// TODO Auto-generated method stub
		while(true) {
			if(num>0) {
        try {
          Thread.sleep(300);//卖一会儿休息一会儿
          System.out.println(Thread.currentThread().getName()+"帮多啦A梦卖第"+num--+"个铜锣烧");
        }catch(InterruptedException e) {
          e.printStackTrace();
        }
			}	
		}	
	}
}
public class SellTest {
	public static void main(String[] args) {
		
		SellThread t = new SellThread();
		
		Thread daxiong = new Thread(t,"大雄");
		Thread xiaojing = new Thread(t,"小静");
		Thread panghu = new Thread(t,"胖虎");
		
		daxiong.start();
		xiaojing.start();
		panghu.start();
	}
}
Console:

大雄帮多啦A梦卖第25个铜锣烧
大雄帮多啦A梦卖第24个铜锣烧
大雄帮多啦A梦卖第23个铜锣烧
大雄帮多啦A梦卖第22个铜锣烧
大雄帮多啦A梦卖第21个铜锣烧
大雄帮多啦A梦卖第20个铜锣烧
大雄帮多啦A梦卖第19个铜锣烧
大雄帮多啦A梦卖第18个铜锣烧
大雄帮多啦A梦卖第17个铜锣烧
大雄帮多啦A梦卖第16个铜锣烧
大雄帮多啦A梦卖第15个铜锣烧
大雄帮多啦A梦卖第14个铜锣烧
大雄帮多啦A梦卖第13个铜锣烧
大雄帮多啦A梦卖第12个铜锣烧
大雄帮多啦A梦卖第11个铜锣烧
大雄帮多啦A梦卖第10个铜锣烧
大雄帮多啦A梦卖第9个铜锣烧
大雄帮多啦A梦卖第8个铜锣烧
大雄帮多啦A梦卖第7个铜锣烧
大雄帮多啦A梦卖第6个铜锣烧
大雄帮多啦A梦卖第5个铜锣烧
大雄帮多啦A梦卖第4个铜锣烧
大雄帮多啦A梦卖第3个铜锣烧
大雄帮多啦A梦卖第2个铜锣烧
大雄帮多啦A梦卖第1个铜锣烧

将共享资源放置在同步方法总中,运行结果与使用代码块的结果是一致的。