Java-多线程与锁机制学习笔记
基本概念
进程与线程
进程是一个应用程序,线程是一个进程中的执行场景/执行单元。一个进程可以启动多个线程。
两者区别:
进程的资源内存独立,不会共享。
线程的堆内存和方法区内存共享,但栈内存独立。
注意:假设启动10个线程,则会有10个栈空间且互不干扰。多线程并发互不影响,提高程序的处理效率。
使用了多线程后,main方法结束,程序并不一定结束。
如:java 运行 System.out.println("");会先启动JVM,JVM再启动 一个主线程调main方法,一个垃圾回收线程。
区别
实现java多线程
通过继承Thread类实现
编写一个类,直接继承java.lang.Thread,重写run方法。
import javax.rmi.ssl.SslRMIClientSocketFactory;
//线程类实现多线程
public class ThreadTest {
public static void main(String[] args) {
Thread mythread=new MyThread();
// mythread.run(); //如果是这样调用,不会开多线程。
mythread.start(); //start方法作用:在JVM中开辟一个新的栈空间,开完start()方法结束。 start方法瞬间就会结束。然后 启动成功的线程会自动调用run()方法。
for(int i=0;i<1000;i++){
System.out.println("主线程--->"+i);
}
}
}
class MyThread extends Thread{
public void run(){ //这个方法即是其他线程的会运行的任务代码。
for(int i=0;i<1000;i++){
System.out.println("分支线程--->"+i);
}
}
}
通过Runnable接口实现
编写一个类,实现java.lang.Runnable接口,实现run方法。
//通过实现runnale接口方式实现多线程 ,这种比较常用,因为runnable 还可以继承其它类。
public class Mythread2 {
public static void main(String[] args) {
MyRunnable r=new MyRunnable();
Thread t=new Thread(r);
t.start();
for(int i=0;i<1000;i++){
System.out.println("main线程---》"+i);
}
}
}
//这是个可运行的类,非线程类
class MyRunnable implements Runnable{
public void run(){
for(int i=0;i<1000;i++){
System.out.println("分支线程---》"+i);
}
}
}
线程的调度
线程运行时的几种状态
线程刚创建出来处于 新建状态,调用start方法后进入 就绪状态,run()方法开始执行标志着线程进入运行状态。当之前占有的cpu时间片占用完后,会重新回到就绪状态抢夺cpu时间片,当再次抢夺到执行权时,继续执行。run完全执行后,此线程进入 死亡状态。
当运行状态遇到阻塞事件(例如sleep,接收用户输入),将发生阻塞进行阻塞状态,将把时间片释放掉。
若阻塞解除,会回到就绪状态抢夺cpu时间片。
就绪状态:又叫可运行状态,表示当前线程具有抢夺cpu时间片的权利(也即执行权)。
进入阻塞状态的方法:java中可通过Thread.sleep()方法,让当前线程休眠。当前线程将进入阻塞状态,放弃cpu时间片,让给其他线程使用。
线程的调度
1.抢占式线程调度模型:哪个线程的优先级比较高,抢到cpu时间片的概率就高一点。java采用的就是这种。
2.均分式线程调度模型:平均分配cpu时间片。每个线程占用的cpu时间片的时间长度相同。
java中线程调度方法:void setPriority(int p)设置线程的优先级
int getPriority()获取线程优先级
最低优先级1
默认优先级5
最高优先级10
t.join() ; 抢位方法,当前线程进入阻塞,t 线程执行。直到t执行完当前线程才可以释放。
静态:yield() 让位方法,当前进程从运行状态回到就绪状态。
线程安全
发生条件
在以下三个条件下可能发生线程安全问题:多线程并发、有共享数据、共享数据有修改的行为。
解决方案:线程排队执行; (线程同步机制)
(线程同步就是线程排队了,牺牲了一部分效率。同步就是需排队,异步就是各自执行各自的,谁也不用等谁。)
线程对象的对象锁
当线程运行遇到sychronized关键字时,其放弃占有的CPU时间片,进入对象锁池找共享对象的对象锁。若找不到进入等待。若找到了便进入就绪状态继续抢夺时间片。
若在实例方法上加sychronized(即类中的方法前加sy),则一定锁的是this,表示整个方法都要同步,可能会无故扩大同步的范围,导致程序执行的效率降低。
sychronized(“abc”),因“abc”在字符串常量池中,所有线程共享,就所有线程全部同步。
Java中三大变量:
实例变量 在堆中
静态变量 在方法区中
局部变量 在栈中
用synchronized保证线程安全:
- 同步代码块代码中: sychronized(线程共享对象){ 同步代码块 }
public void withdraw(double money){
synchronized (obj){ //只在执行下面代码的时候锁
double before=this.getNum();
double now=before-money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setNum(now);
}
}
- 实例方法上使用:表示共享对象一定是this
public synchronized void withdraw(double money){ //在执行这个方法的时候锁 直接锁了this的全部变量
{
double before=this.getNum();
double now=before-money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setNum(now);
}
}
- 在静态方法上使用:表示 找类锁 。类锁只有一把,不论对象多少。(目的是保证静态变量的安全)(排他锁)
public synchronized static void withdraw(double money){ //锁住后,别人操作不了这个静态方法
}
死锁
sychronized嵌套使用,容易发生死锁现象。
public class Deadlock {
public static void main(String[] args) {
Object o1=new Object();
Object o2=new Object();
Thread t1=new Mythread1(o1,o2);
Thread t2=new Mythread2(o1,o2);
t1.start();
t2.start();
}
}
class Mythread1 extends Thread{
Object o1;
Object o2;
public Mythread1(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
public void run(){
synchronized (o1){
try {
Thread.sleep(1000); //sleep保证了死锁,如果不加,也有可能死锁
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}}
class Mythread2 extends Thread{
Object o1;
Object o2;
public Mythread2(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
public void run(){
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
线程安全问题解决方案
线程安全问题如何解决: 直接选择线程同步会严重影响用户体验。
方案一: 尽量使用局部变量代替实例变量和成员变量。
方案二: 如果必须是实例变量,则可以考虑创建多个对象,这样实例变量的内存就不共享了。
方案三:若不可用局部变量,且不可创建多个对象,这个时候就只能选择sychronized了。线程同步机制。
守护线程
守护线程: 指后台运行的线程
代表性守护线程:垃圾回收
(定时数据自动备份)
守护线程的特点:一般守护线程是一个死循环,用户线程只要结束,守护线程自动结束。
public class GuardThread {
public static void main(String[] args) {
Thread t=new BakDataThread();
t.setName("备份数据的线程");
t.setDaemon(true); //守护线程
t.start();
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread{
public void run(){
int i=0;
while(true){
System.out.println(Thread.currentThread().getName()+"--->"+(++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Callable方法获取线程的返回值
用Callable接口实现多线程, 这种方式可以获取线程的返回值。( run是void无返回值)
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask; //JUC包下的属于java并发包
public class Callable_Thread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask task=new FutureTask(new Callable() {
@Override
public Object call() throws Exception { //相当于run方法,但有返回值。
System.out.println("begin !");
Thread.sleep(1000);
System.out.println("end !");
return 10;
}
});
Thread t=new Thread(task);
t.start();
System.out.println(t);
Object o=task.get();
System.out.println(o);
}
}
线程的等待与唤醒
线程的等待与唤醒使用Object类中的wait方法和notify方法。
Obeject o=new Object();
o.wait(); 表示让正在执行对象的线程 (可能若干个线程在操作这个对象)进入等待,直到notify唤醒为止。此操作会释放o对象上的锁。
o.notify(); 唤醒对象o上等待的线程,只是通知等待的线程可以继续操作了。不会释放o对象上的锁。
o.notifyAll();唤醒o对象上的所有线程。
使用场景: 生产者生产商品到仓库;消费者从仓库中消费商品;
要达到供需关系平衡,需要仓库 来调用wait与notify方法。
当然,仓库还存在线程安全的问题,这两个方法需要建立在线程同步的基础之上。
import Java_bilibili.Collections_and_Map.ArrayListTest; //生产者--消费者--仓库 模型代码
import java.util.ArrayList;
import java.util.List;
public class PC_Thread {
public static void main(String[] args) {
List l=new ArrayList<>();
Thread t1=new Thread(new Producer(l));
Thread t2=new Thread(new Consumer(l));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
class Producer implements Runnable{
private List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
while(true){
synchronized (list){
if(list.size()>=10){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else{
list.add(1);
System.out.println(Thread.currentThread().getName()+"---->生产+1"+" 剩余--"+list.size());
list.notify();
}
}
}
}
}
class Consumer implements Runnable{
private List list;
public Consumer(List list) {
this.list = list;
}
public void run() {
while(true){
synchronized (list){
if(list.size()==0){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else {
list.remove(0);
System.out.println(Thread.currentThread().getName()+"---->消费+1"+" 剩余--"+list.size());
list.notify();
}
}
}
}
}
多线程取钱样例代码
在并发取钱条件下,若不使用锁机制,可能造成取钱的bug
import java.util.Objects;
public class AccountThread extends Thread{
private Bank_Accout act;
public AccountThread(Bank_Accout act) {
this.act=act;
}
public void run(){
double m=5000;
act.withdraw(5000);
System.out.println("取款成功,余额"+act.getNum());
}
}
class Bank_Accout {
private String actno;
private double num;
Object obj=new Object();
public Bank_Accout(String actno, double num) {
this.actno = actno;
this.num = num;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getNum() {
return num;
}
public void setNum(double num) {
this.num = num;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Bank_Accout that = (Bank_Accout) o;
return
actno.equals(that.actno);
}
@Override
public int hashCode() {
return Objects.hash(actno);
}
public void withdraw(double money){
// //并发取钱,出现bug
// double before=this.getNum();
// double now=before-money;
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// this.setNum(now);
//排队取钱,方为上策。
// synchronized ("ss"){ // 所有线程全部同步,一人取钱,全球等待
// synchronized (this){ //this是排队线程共享的对象 多线程对this同一账户操作时等待
synchronized (obj){ //obj是 排队线程共享的对象 多线程对同一账户操作时等待
double before=this.getNum();
double now=before-money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setNum(now);
}
}
}