Java多线程以及同步死锁
1. 多线程
学习前我们要清楚什么是线程和进程,两者有什么关系?线程是如何创建的,它的作用是什么,它的常用方法是什么?线程的同步和死锁可以解决什么问题。
2. 了解基本的概念:
线程(Thread):线程是进程中最小的调度的单元(单位),cpu控制的最小的执行单元。轻量级的进程。任何一个程序都至少有一个线程在用,多个线程共享内存。多线程切换消耗的资源少。
进程(Processor):活动的程序,进程;已经启动,进驻到内存中,正在使用的程序,:进程;每个进程拥有独立的内存空间。
简单举个例子:进程相当于某工厂的某生产车间,线程就是该车间里的流水线。线程和进程的关系,这里我们引入了一个概念同步:
1.一个进程可以有多个线程,但至少有一个线程;而一个线程只能在一个进程的地址空间内活动。
2、资源分配给进程,同一个进程的所有线程共享该进程所有资源。
3、CPU分配给线程,即真正在CPU处理器运行的是线程。
4、线程在执行过程中需要协作同步,不同进程的线程间要利用消息通信的办法实现同步。
3.学习线程的创建办法:
- 无返回值创建线程的两种方式:第一种可以去直接继承Thread类,重写run方法,创建线程对象,启动线程(start())对象;第二种可以去实现Runnable接口,重写run方法。具体通过代码:
//继承Thread类
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
super(name); //调用父类构造方法
}
@Override
public void run(){
for (int i = 0; i <=10; i++) {
System.out.println(Thread.currentThread().getName()+"在玩游戏"+i);
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
// myThread.run(); 执行方法,不是 启动线程
myThread.start();//启动线程
MyThread1 myThread1 = new MyThread1();
myThread1.start();
for (int i = 0; i <=10; i++) {
System.out.println("主线程执行"+i);
}
}
}
//实现Runnable接口,重写run方法
public class MyRun1 implements Runnable{
@Override
public void run() {
for (int i = 0; i <=10; i++) {
System.out.println(Thread.currentThread().getName()+"正在思考");
}
}
public static void main(String[] args) {
MyRun1 myRun1 = new MyRun1();
Thread t1 = new Thread(myRun1);
Thread t2 = new Thread(myRun1);
t1.start();
t2.start();
}
}
通过以上学习,你发现多个线程在执行时,它是没有先后执行顺序的。它由新建进入就绪状态,在运行时它们会抢占Cpu时间片,谁抢到谁先运行,直到进程结束。
学习过程中,大家发现没有run()方法和start()两个方法,这两个方法在线程中的区别如下:
- t.run() 不会启动线程,只是普通的调用方法而已。不会分配新的分支栈。(这种方式就是单线程。)
- t.start() 方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。
启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的哈。
- 有返回值创建线程时,我们实现Callable接口可以获取线程的返回值,实现Callable接口,要和FutrueTask类连用去获取线程返回值。具体方法如下:
public class MyCall implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName()+"小狗正在吃瓜");
return "吐出了瓜子";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCall call = new MyCall();
// FutureTask类 获得线程返回值
FutureTask<String> futureTask = new FutureTask<>(call);
Thread t1 = new Thread(futureTask);
t1.start();
String s = futureTask.get();
System.out.println("吃完西瓜后:小狗"+s);
}
}
- 学习线程的常用API,这里我们引用一下。
- 了解线程的状态,进行基本的线程调度操作。
- 线程的五种状态,明白在线程执行时对应的操作是:
体系图看这里:
- 对线程进行调度的基本操作:
1.调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
super(name); //调用父类构造方法
}
@Override
public void run(){
for (int i = 0; i <=10; i++) {
System.out.println(Thread.currentThread().getName()+"在玩游戏"+i);
}
}
public static void main(String[] args) {
// 通过构造犯法
MyThread myThread = new MyThread("西门吹雪");
myThread.setPriority(Thread.MAX_PRIORITY);//优先级最大为1
myThread.start();
System.out.println("优先级1:"+myThread.getPriority());
// 主线程优先级 默认为5
System.out.println(Thread.currentThread().getName()+":"+Thread.currentThread().getPriority());
}
}
2.线程阻塞
sleep(),线程休眠,会让当前线程处于阻塞状态,指定时间过后,线程就绪状态。yield()暂停当前正在执行的线程对象,并执行其他线程,yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。interrupt:中断线程,仅仅发送了一个中断的信号,当碰到wait(),sleep方法时,清除中断标记,抛出异常。setDaemon:设置线程为后台(守护)线程。
- 了解多线程数据安全问题:
以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。
最重要的是: 你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。以下可能会产生数据安全问题:
- 条件1:多线程并发。
- 条件2:有共享数据。
- 条件3:共享数据有修改的行为。
我们举例子来验证,例如:我和其余两位同时取一个账户的钱,我取钱后数据还没返回给服务器,两位同学依次再取,这个时候显示的余额为后面同学取钱的余额。显然这样不对,我们如何解决?线程排队执行?用线程同步,使用Synchronized同步实现?大家可以理解一下锁:
Lock是显示锁,需要自己手动开启和关闭,synchronized是隐式锁,出了作用于自动释放,无需自己释放。
Lock只能锁代码块,synchronized可以锁代码块和方法,使用Lock锁,JVM将花费更少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
//建立一个账户类,写一个取钱方法
public class Account {
private String no;//账号
private double money;//金额
public Account() {
}
public Account(String no, double money) {
this.no = no;
this.money = money;
}
private Lock lock = new ReentrantLock(); //创建锁对象
public void getMoney(double m){
synchronized (this){ //利用同步,解决多线程安全问题
double before = this.money;
double after = before - m;
lock.lock(); //上锁
try {
Thread.sleep(300); //线程休眠
} catch (InterruptedException e) {
System.out.println(e.getMessage());
} finally {
lock.unlock();//关锁
}
this.money = after;
System.out.println(Thread.currentThread().getName()+"取钱:"+ m + "余额:" + this.money);
}
}
}
写Thread继承类,重写run()方法,写测试类进行测试。
public class AccountThread extends Thread {
private Account account;
public AccountThread(Account account){
this.account = account;
}
@Override
public void run() {
account.getMoney(500);
}
public static void main(String[] args) throws InterruptedException {
Account account = new Account("123456", 3000);
AccountThread t1 = new AccountThread(account);
AccountThread t2 = new AccountThread(account);
AccountThread t3 = new AccountThread(account);
t1.start();
t2.start();
t3.start();
}
}
结果:
- 死锁:当多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,从而导致两个或者多个线程都在等待对方释放资源,都停止执行的情况。最简单的,让某一个同步块同时拥有“两个以上的对象的锁”的时候,就可能会发生死锁问题,这个时候就引入一个经典案列:哲学家就餐问题。解决该问题上锁,要求尽量不要让 一个同步代码块 同时拥有“两个以上的对象的锁”,实例如下:
//定义生产者消费者
public class Producer extends Thread {
private Stock stock;
public Producer(Stock stock) {
this.stock = stock;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
stock.push();
}
}
}
public class Customer extends Thread{
private Stock stock;
public Customer(Stock stock) {
this.stock = stock;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
stock.pop();
}
}
}
//解决生产者消费者问题,方法1上Stock锁。
public class Stock {
private int count = 0;//库存
// 生产
public synchronized void push(){
if(count<10){
count++;
this.notifyAll();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"库存不足,开始生产棒棒糖"+count);
}else{
System.out.println(Thread.currentThread().getName()+"库存已足,停止生产");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 消费
public synchronized void pop(){
if(count>0){
count--;
this.notifyAll();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"库存足够,消费一个棒棒糖"+count);
}else{
System.out.println(Thread.currentThread().getName()+"库存不足,继续生产");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Stock stock = new Stock();
Producer producer1 = new Producer(stock);
Producer producer2 = new Producer(stock);
Customer customer1 = new Customer(stock);
Customer customer2 = new Customer(stock);
Customer customer3 = new Customer(stock);
producer1.start();
producer2.start();
customer1.start();
customer2.start();
customer3.start();
}
}
//方法2:写一个阻塞队列,确定生产消费长度
//实现Runnable接口,用put()和take()方法。
public class ArrayBq {
public static void main(String[] args) {
// 阻塞队列:
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
Thread prouder = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
queue.put(i);
System.out.println("生产牛肉:"+i);
// Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread customer = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
int values = queue.take();
System.out.println("消费牛肉:" + values);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
prouder.start();
customer.start();
}
}
学到这里本篇就结束了,感谢大家阅览!