20180125:通过实现Runnable接口创建线程、多线程安全与同步问题、使用synchronized关键字实现同步、死锁问题、Lock实现同步

本文介绍了Java中通过实现Runnable接口创建线程的方法,并探讨了线程同步的重要性及其实现方式,包括使用synchronized关键字、Lock接口及其实现类ReentrantLock,还详细解释了读写锁的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、通过实现Runnable接口创建线程

 1. 定义实现Runnable接口的类

  Runnable接口中有一个run()方法;用来定义线程运行体,定义自己的线程类实现Runnable接口并重写run()方法;

在测试类中创建线程类实例的时候将这个类的实例传递到线程实例内部,然后再启动;接下来举一个实现Runnable类的线程类来

看一下这个东西。

 实现Runnable类:

package runnable;

public class MyThread implements Runnable{
	
	public void run(){
		System.out.println("进入子线程。。。");
		for(int i=0;i<10;i++){
			if(i%2 == 0){
				System.out.println(i);
			}
		}
		System.out.println("子线程执行结束。。。");
	}
	
}
测试类:

package runnable;

public class MyThread implements Runnable{
	
	public void run(){
		System.out.println("进入子线程。。。");
		for(int i=0;i<10;i++){
			if(i%2 == 0){
				System.out.println(i);
			}
		}
		System.out.println("子线程执行结束。。。");
	}
	
}
测试结果:




二、优势

1. 避免了java单继承的局限性;

2. 使用实现Runnable接口的方式创建线程时,可以为相同程序代码的多个线程提供共享的数据(资源共享)

三、多线程安全与同步问题

当run()方法体内的代码操作到了成员变量(共享数据)时,就可能会出现多线程安全问题(线程不同步问题)。

线程的同步
 1.在java语言中,引入对象互斥锁,引入对象互斥锁的概念,保证共享数据操作的完整性。每个对象都对应于一个可称为“互斥锁”
的标记,这个标记保证在任一时刻,只能有一个线程访问对象。

 2. 关键字synchronized用来与对象的互斥锁关联。当某个对象用synchro修饰时,表明该对象在任一时刻只能由一个线程访问

(这个对象就变成了同步对象)。

 3. 一个方法使用关键字synchroni修饰后,当一个线程A使用这个方法时,其他线程想使用这个方法就必须等待,直到线程A使用该方法(前提

是这些线程使用的是同一个同步对象)。

线程同步后效率会变低,变低的原因是:

 1. 会丧失java多线程的并发优势,在执行到同步代码块(或同步方法时),只能有一个线程执行,其他线程必须等待执行同步代码块(或同步方法)

的线程释放同步对象的锁才能执行。

2. 其他等待所释放的线程会不断检查锁的状态,也浪费了一定的系统资源


同步方法的同步对象:

 1.对于非静态方法来说,this当前对象充当了同步对象

非静态方法的demo,卖火车票,每次只能卖一张票,访问时:

package synchronizeddemo;

public class TicketDemo implements Runnable{
	
	private int ticket=5;

	
	public void run() {
		synchronized (this) {
			for(int i=0;i<100;i++){
				
				if(ticket>0){
					
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					ticket--;
					System.out.println(Thread.currentThread().getName()+"卖了一张票,剩余票数为:"+ticket);
				}
				
			}
		}
		
		
	}
}
线程的执行:

package synchronizeddemo;

public class Run {
	
	public static void main(String[] args) {
		
		TicketDemo td = new TicketDemo();
		//加锁之后能够完成对共享变量的互斥访问
		new Thread(td,"A线程").start();
		new Thread(td,"B线程").start();
		new Thread(td,"C线程").start();
	}
	
}
测试结果为:


 2. 静态方法的同步对象成为"类对象",类对象代表的是这个类本身,所有通过类实例化的普通对象共享这个类对象,锁住的前提是多个线程共享一个对象。

 写一个关于静态方法的同步的demo:

package staticsync;

public class PersonRunnable implements Runnable{

	private int money=1000;
	
	
	public void run() {
		makeMoney();
	}
	
	
	public static synchronized void makeMoney(){
		
		try {
			System.out.println(Thread.currentThread().getName()+"帮你赚钱了。。。");
			Thread.sleep(1000);
			//money+=1000;
			System.out.println(Thread.currentThread().getName()+"帮你赚了"+"钱");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

 测试类:
package staticsync;

public class Run {
	
	public static void main(String[] args) {
		
		PersonRunnable pr1 = new PersonRunnable();
		PersonRunnable pr2 = new PersonRunnable();
		PersonRunnable pr3 = new PersonRunnable();
		
		new Thread(pr1,"马云").start();
		new Thread(pr2,"马化腾").start();
		new Thread(pr3,"王健林").start();
	}
	
}	
测试结果为:



注:同步代码块或者同步方法的区域成为临界区.

六、死锁问题(线程中的“假死”现象)

 线程死锁的原因:

 线程1锁住资源A等待资源B,线程2锁住资源B等待资源A,两个线程都在等待自己需要的资源,而这些资源被另外的线程锁住,这些线程你等我,我等你,谁也不愿意让出资源,这样死锁就产生了。

 死锁中经典的哲学家进餐问题(哲学家进餐Demo):

package deathlock;

public class Knife {
	
	public void knifeSay(){
		System.out.println("我拿到刀子了。。。。");
	}
	
}

package deathlock;

public class Fork {
	
	public void forkSay(){
		System.out.println("我拿到叉子了。。。");
	}
	
}

实现一个线程类,见证一下死锁问题的发生:

package deathlock;

//模拟哲学家进餐问题,产生死锁,解决死锁是加重锁的力度
public class EatRunnable implements Runnable{
	
	private boolean flag=false;
	private static Knife knife = new Knife();
	private static Fork fork = new Fork();
	
	public void setFlag(boolean flag) {
		this.flag = flag;
	}
	
	public void run(){
		
		if(flag){
			synchronized (knife) {
				knife.knifeSay();
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				synchronized (fork) {
					fork.forkSay();
				}
			}
			
			System.out.println(Thread.currentThread().getName()+"吃完饭啦。。");
		}else{
			synchronized (fork) {
				fork.forkSay();
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				synchronized (knife) {
					knife.knifeSay();
				}
			}
			
			System.out.println(Thread.currentThread().getName()+"吃完饭啦。。。");
		}
		
		
	}
	
}

测试类:

package deathlock;

public class Run {
	
	public static void main(String[] args) {
		
		EatRunnable er1 = new EatRunnable();
		EatRunnable er2 = new EatRunnable();
		er1.setFlag(true);
		new Thread(er1,"康德").start();
		new Thread(er2,"亚里士多德").start();
	}
	
}

测试结果为:



从程序运行结果中可以看出,两个哲学家对象一个拿到了刀子,一个拿到了叉子,两个线程若想继续向下运行,还需要对方手里的“锁”,但这两个线程互不相让,导致程序无法向下运行,即发生了所谓的“死锁现象”。

解决办法还得从同步方法入手,在线程类中,由于“锁上加锁”就容易导致死锁问题的发生。因此,可以调整两个同步对象的顺序,使这两个哲学家获得“同步锁”的顺序是一样的,这样就可以使得在一方哲学家拿到锁之后,另一个在哲学家在执行时一样拿不到锁,就避免了“死锁”问题的发生;另外,这个问题发生的情况就是“锁里有锁”的情况,可以把锁里面的锁去除,放在外面,加大锁的力度即可。


七、Lock锁实现同步

 java,util,concurrent.locks包下的相关接口

1.Lock接口
 通常使用Lock接口中的方法用来获取锁,其中lock()方法是使用的最多的一个方法,意思就是获取当前对象的锁,获取锁之后接下来的代码就是只能一个线程去调用,在需要同步的代码段之后还需要调用unlock()方法释放该锁。同时加锁以及需要同步的代码段可以放在try语句块中,在finally语句块中调用unlock()方法释放锁,这样可以保证一个线程在执行完代码之后一定能够调用unlock()方法释放锁。
 2.ReentrantLock类(“可重入锁”)
ReentrantLock是实现了Lock接口的类。

写一个Demo:
package lock;

import java.util.concurrent.locks.ReentrantLock;

public class TicketLock implements Runnable{
	
	private int ticket=5;
	private ReentrantLock lock = new ReentrantLock();
	
	public void run(){
		
		for(int i=0;i<100;i++){
			try {
				lock.lock(); //获取锁,在需要同步的代码块上获取lock锁
				if(ticket>0){
					Thread.sleep(1000);
					ticket--;
					System.out.println(Thread.currentThread().getName()+"卖了一张票,剩余票数为:"+ticket);
				}
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}finally{
				lock.unlock(); // 执行完加锁的代码块后要将锁释放掉
			}
		}
		
		
	}
	
}

写一个测试卖票类:
package lock;

public class Run {
	
	public static void main(String[] args) {
		
		TicketLock tl = new TicketLock();
		
		new Thread(tl,"A窗口").start();
		new Thread(tl,"B窗口").start();
		new Thread(tl,"C窗口").start();
	}
	
}
测试结果为:



从测试结果可以看出:lock锁同样也可以实现同步的功能。

3.ReadWriterLock接口
包括两个方法:

 Lock readLock(): 用来获取“读锁",返回一个Lock对象;

 Lock writeLock(): 用来获取"写锁",返回一个Lock对象;

 4.ReentrantReadWriteLock类
 是ReadWriteLock的实现类。

在实现读写锁之前先介绍下读写锁:

1.若有一个线程已经占用了该锁,则此时其他线程若要申请写锁,则申请写锁的线程会一直等待释放读锁,但其他线程申请读锁是可以的;

举个例子:一个银行账户在查询账户余额时可以有多个人查看信息,这在不泄露隐私的前提下是允许的,但在查看的时候,不能允许有人对银行账目进行修改,无论是存钱还是取钱都不被允许,因为在查看过程中对存款金额进行了修改之后,由于在查询时已经对数据进行了访问,但此时数据已经发生了改变,这就导致了查询信息的不准确,影响用户体验。

2. 若有一个线程已经占用了写锁,则此时其他线程若申请写锁或者读锁,则申请的线程会一直等待释放写锁。

这同样容易理解,在现实生活中,比如后台人员需要服务器数据做出合理修改的时候,在客户端用户的查询窗口自然会被关闭,知道修改维护完成好之后才会向用户开放,这是因为在对数据进行修改时,用户查询到的数据可能会发生混乱,读到的数据八成是不正确的,同时还可能造成数据的损坏,同时,在修改数据时也不允许其他人操作这些数据,只对有权修改的人开放。

 5.接下来就写一个关于读写锁的demo

 读取数据与修改数据,一个线程读取时可以有多个线程读取,但不允许写入数据;在写入数据时既不允许其他线程读取数据

 也不允许其他线程读取数据:

package rwlock;

import java.util.Random;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class InfoRunnable implements Runnable{
	
	private static int data;
	private boolean flag=false;
	
	//创建读写锁对象,该属性对象可以获取读锁或写锁
	ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
	
	public void setFlag(boolean flag) {
		this.flag = flag;
	}
	
	public void run(){
		
		if(flag){
			for (int i = 0; i < 10; i++) {
				writeData();
			}
		}else{
			for (int i = 0; i < 10; i++) {
				readData();
			}
		}
	}
	
	//写数据
	public void writeData(){
		
		try {
			rw.writeLock().lock(); //获取写锁,若已经有线程占用了该写锁,则此时其他线程要求获得写锁的线程会一直等待释放写锁
			System.out.println(Thread.currentThread().getName()+"准备写入数据...");
			Thread.sleep(1000);
			data = new Random().nextInt(10);  //模拟写入数据
			System.out.println(Thread.currentThread().getName()+"写数据完毕。。。。"); 
			
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			rw.writeLock().unlock(); //执行完之后释放写锁啊。
		}
		
	}
	
	//读取数据
	public void readData(){
		
		try {
			rw.readLock().lock(); //获取读锁,若有一个线程占用了这个读锁,其他线程在申请写锁时不被允许,但仍允许申请读锁
			System.out.println(Thread.currentThread().getName()+"准备读取数据");
			Thread.sleep(1000);
			System.out.println(Thread.currentThread().getName()+"读取到的数据为:"+data);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			rw.readLock().unlock();
		}
	}
	
}
写一个测试类,多个线程读取数据,多个线程写入数据:
package rwlock;

public class Run {
	
	public static void main(String[] args) {
		
		InfoRunnable ir1 = new InfoRunnable();
		ir1.setFlag(true);
		new Thread(ir1,"写入线程1").start();
		new Thread(ir1,"写入线程2").start();
		
		InfoRunnable ir2 = new InfoRunnable();
		InfoRunnable ir3 = new InfoRunnable();
		new Thread(ir2,"读取线程3").start();
		new Thread(ir3,"读取线程4").start();
	}
	
}
读取结果为:



从测试结果可以看出:在读取数据时多个线程可以同时读取到数据,而且读取的数据值肯定是相同的,但在读取数据时没有写数据进程的执行;在写入数据时仅有一条线程一口气执行完了写入数据的操作,没有读数据或写数据进程乱入。。

补充:

synchronized与Lock的区别和联系:

1.Lock是一个接口,而synchronized是java中的关键字,synchronized是内置的语言实现。

2.sychronized在发生异常时,会自动释放线程占有的锁,而Lock在发生异常时,若没有主动通过unlock()去释放锁则很可能造成死锁现象,因此使用lock时需要在finally块中去释放锁。

3.Lock可以提高多个线程进行读操作的效率诶。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值