Java学习笔记 -多线程1
概述
内存
-
进程之间的内存独立不共享
-
Java中两个线程:
1)共享堆内存和方法区
2)栈内存各自独立 -两个栈
多线程并发下,数据修改会存在线程安全问题,如何解决?
-
线程排队执行,用排队执行解决
-
这种机制称为:线程同步机制
-
异步编程模型:多线程并发
-
同步编程模型:线程排队执行
创建新线程的三种方法
编写自定义线程类
public class ThreadTest01{
public static void main(String[] args) {
MyThread myThread = new MyThread();
//start()方法的作用:开辟一个新的栈空间,开辟完就结束了
//有了新的栈空间就代表线程启动成功,此时run()方法和main()方法具有了相同的地位
//如果只调用run()方法,而没有先分配栈空间,则没有新的线程,run()会使用主线程的栈
myThread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程运行: " + i);
}
}
}
//继承Thread类,重写run()方法
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程运行: " + i);
}
}
}
直接实现Runnable接口
public class ThreadTest02 {
public static void main(String[] args) {
//面向接口编程(推荐)
Thread t1 = new Thread(new MyRunnable());
//匿名内部类的实现方式
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程运行t2: " + i);
}
}
});
t1.start();
t2.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程运行: " + i);
}
}
}
//直接实现Runnable接口,重写run()方法
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程运行t1: " + i);
}
}
}
实现Callable接口
相比之前的两种方式,这个实现方式有明显的不同:
- 优点:可以获取到线程的执行结果
- 缺点:当前线程受阻,需要等待t线程执行完毕返回结果,效率较低
public class FutureThreadTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1.创建一个“未来任务类对象”,需要传递一个Callable接口的实现对象
FutureTask task =new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName() + "执行");
Thread.sleep(1000 * 5);
System.out.println(Thread.currentThread().getName() + "结束");
return 1; //自动装箱
}
});
//2.创建线程对象
Thread t1 = new Thread(task);
t1.setName("t1");
t1.start();
//3.在主线程中如何获取t1线程的执行结果?
//使用task.get()方法
task.get();
//因为要切换到另一个线程执行,当前线程需要阻塞,等待另一个线程执行完毕后,返回执行结果
System.out.println("主线程执行");
}
}
线程常用方法
- void setName(String name)
- String getName()
- static Thread currentThread() 返回对当前正在执行的线程对象的引用
- static void sleep(long millis) 参数是毫秒,在哪个线程调用,就让哪一个线程进入休眠
- void interrupt() 通过产生异常的方式,中断线程的睡眠
示例程序:
public class ThreadTest04 {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable2());
Thread t2 = new Thread(new MyRunnable2());
//设置线程的名字
t1.setName("t1");
t2.setName("t2");
//获取线程的名字
System.out.println(t1.getName());
System.out.println(t2.getName());
t1.start();
t2.start();
}
}
class MyRunnable2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName() +": " + i);
}
}
}
提前终止线程的方法
使用interrupt()方法
public class ThreadTest05 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000 * 60 * 60 * 24);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread tmp = Thread.currentThread();
System.out.println(tmp.getName() + "线程 运行了");
}
});
//产生新的线程
t.setName("t");
t.start();
//主线程睡眠5s之后,叫醒t线程
Thread.sleep(1000 * 5);
//中断睡眠,通过产生异常的方式
t.interrupt();
}
}
设置标志信号量 -推荐
public class ThreadTest06 {
public static void main(String[] args) {
MyRunnable3 myRunnable3 = new MyRunnable3();
Thread t = new Thread(myRunnable3);
t.setName("t");
t.start();
//模拟5s后终止分支进程
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
myRunnable3.run = false;
}
}
class MyRunnable3 implements Runnable{
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if(run){
System.out.println(Thread.currentThread().getName() + "-->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
return;
}
}
}
}
线程同步机制
什么情况下需要线程同步机制?
多个线程同时修改同一内存这的数据时会发生线程安全问题,需要线程同步机制。
正是因为这一点,所以局部变量一定不会存在线程安全问题。
线程同步代码块 synchronized()
()中传递的“数据”相当关键
这个数据必须是多线程共享的对象,才能达到多线程排队
假设只希望t1 t2 t3排队 t4 t5不需要排队
那么一定要在()写一个t1 t2 t3共享的对象,而这个对象对于t4 t5来说是不共享的
强调: 共享 对象
对象:参数应该是一个对象
共享:多个需要同步的线程执行到synchronized()检测的对象应该是同一个
举一个例子:字符串"abc",那么所有线程都会检测字符串常量池中的"abc",则所有线程都处于线程排队的状态
注意事项
-
同步代码块中的代码要尽可能的短小,提高执行效率
-
如果方法没有包含synchronized() 则此方法在执行的时候不会去锁池了寻找对象锁,会直接执行
-
在实例方法上可以使用synchronized修饰:
例如:public synchronized void withdraw(double money);
此时:锁默认是当前对象this,且方法体中全部的代码都需要同步;所以这种方法不灵活 -
如果synchronized修饰在静态方法上,表示类锁,类锁只有一把;而对象锁每一个对象都有一把
示例程序:
public class Accout {
private double balance;
public Accout() { }
public Accout(double balance) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public void withdraw(double money){
double after;
synchronized (this){
double before = getBalance();
after = before - money;
//模拟网络延迟1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
setBalance(after);
}
System.out.println(Thread.currentThread().getName() + "取款:" + money + " 余额: " + after);
}
}
public class AccoutThread {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable(new Accout(1000));
//实例化两个取款线程类,模拟线程并发
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
//取款线程类
class MyRunnable implements Runnable{
private Accout accout;
public MyRunnable() {
}
public MyRunnable(Accout accout) {
this.accout = accout;
}
public Accout getAccout() {
return accout;
}
public void setAccout(Accout accout) {
this.accout = accout;
}
@Override
public void run() {
accout.withdraw(500);
}
}
总结 -解决线程安全问题的步骤?
-
synchronized会让程序的执行效率降低,则系统的用户吞吐量降低,用户体验差。在不得已的情况下再选择线程同步机制
-
第一种方案:尽量使用局部变量代替"实例变量"和"静态变量"
-
第二种方案:如果必须使用实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了
-
第三种方案:synchronized