学习到多线程了,感觉多线程的目的是为了实现并发,让程序不是鱼贯而入,而是齐车并驾。
一、如何实现多线程程序
①:继承Thread类,重写run方法设置线程任务,使用strat方法启动线程。
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()); //打印当前线程名字
}
}
=====================================================================================
public class MyThreadDemo {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start(); //开启线程任务
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
②:实现Runnable接口,重写run方法,然后在main方法中创建Thread对象,把Runnable对象作为构造方法参数。再使用Thread对象的start方法开启线程。
public class RunnableImpl implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"----"+i);
}
}
}
=====================================================================================
public class RunnableDemo {
public static void main(String[] args) {
RunnableImpl runnable = new RunnableImpl();
Thread th = new Thread(runnable);
th.start();
for (int i = 0; i < 60; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
Runnable接口实现多线程的好处:
1.消除了继承的局限,若用继承Thread来实现多线程,那么该类就不能继承其他类了,因为类是单继承的
2.提高耦合性,每个实现Runnable接口的实现类,可以定义不一样的线程任务,而不是有需求变更就去改Thread类的子类,把设置线程任务和开启新线程分开了。
Runnable实现类——>设置线程任务
Thread对象——>开启新线程
二、线程安全问题
1.synchronized同步
多个线程访问同一资源时,可能出现线程安全问题
比如说,电影院三个线程卖一张票,三个线程同时并发执行到了卖票这个操作,但没有执行到票数减一的操作,此时三个线程都以为还有一张票,却有两个窗口买了两张幽灵票,这就是线程安全问题
解决线程安全问题方法:
①:synchronized代码块:obj是锁对象,作用是,在线程执行到synchronized时获取锁,那么其他线程执行到synchronized时,因为锁已经被获取走了,那么其他进程就进入阻塞状态。
public void run() {
Object obj = new Object();
synchronized (obj){
//可能会出现线程安全的代码块
}
}
②:synchronized方法:
同步锁是谁? 对于非static方法,同步锁就是this。 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
public class Synchronized extends Thread{
@Override
public void run() {
method();
}
public synchronized void method(){
//可能会出现线程安全的代码块
}
}
2.Lock锁
Lock锁是一个接口,其实现类有:ReentrantLock。
常用方法:
void lock() 获取锁。
void unlock() 试图释放此锁。
当有try/catch处理异常时,我要把unlock放在finally代码块里面,不然如果抛出了异常,那么这个线程就进入死锁了。
三、线程的状态
四、线程等待和线程唤醒
如果线程1必须要线程2完成某项业务后再执行,那么我们可以让线程1进入等待状态,线程2结束某项业务后再唤醒线程1.
无限等待:
Object类中有wait()方法,让当前进程进入等待状态。
void wait()
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
随机单个唤醒:
Object类中有notify()方法,唤醒等待状态的线程。
void notify()
唤醒在此对象监视器上等待的单个线程。
若有多个等待进程,则随机唤醒一个
计时等待:
void wait(long timeout)在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。
全部唤醒:
void notifyAll()
唤醒在此对象监视器上等待的所有线程。
五、使用线程通信——等待唤醒的好处
单纯使用synchronized的话,
资源就只能等拥有锁的线程使用完毕,但是同时其他线程会来尝试竞争资源,但是并没有对象锁,所以只能继续进入阻塞状态。
各线程会不断地去竞争cpu资源,未拥有对象锁的线程就一直做着无用功,造成了不必要的资源浪费。
但是使用wait方法让线程进入等待状态的话,Waiting的线程就不会来争夺cpu资源,不再参与调度,而是老老实实等待有线程去唤醒(notify方法)它
六、线程池
由于线程的创建、销毁是比较占用资源的,所以为了能反复使用线程,有了线程池的概念。
使用方法:
1.通过线程池工厂中的静态方法设置线程池中的线程数目,并返回一个线程池对象。
static ExecutorService newFixedThreadPool(int nThreads)
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。
ExecutorService service = Executors.newFixedThreadPool(2)
2.创建Runnable实现类对象,当然也能用匿名内部类的方法在submit中创建实现类对象。
MyRunnable r = new MyRunnable();
匿名内部类的方法传递Runnable实现类对象
service.submit(new Runnable() {
@Override
public void run() {
}
});
3.使用线程池的submit方法,将Runnable实现类对象提交给线程池,然后在线程池中调用线程执行Runnable实现类对象中的run方法
service.submit(r);
Future<?> submit(Runnable task)
提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
七、Lambda函数编程
当接口中只有一个方法时,我们可以使用Lambda函数编程来实现接口内的功能,简化了代码。
//使用匿名内部类实现接口
Arrays.sort(array, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge()-o2.getAge();
}
});
//使用Lambda表达式实现接口
Arrays.sort(array,(Person p1,Person p2)->{return p1.getAge()-p2.getAge();});
为了排序, Arrays.sort 方法需要排序规则,即 Comparator 接口的实例,抽象方法 compare 是关键;
为了指定 compare 的方法体,不得不需要 Comparator 接口的实现类;
为了省去定义一个 ComparatorImpl 实现类的麻烦,不得不使用匿名内部类; 必须覆盖重写抽象 compare 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
实际上,只有参数和方法体才是关键,也就是要做什么才是关键,怎么实现的并不是重点
使用方法是,(参数列表)->{抽象方法的实现}
省略规则 在Lambda标准格式的基础上,使用省略写法的规则为:
1. 小括号内参数的类型可以省略
2. 如果小括号内有且仅有一个参,则小括号可以省略;
3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号
1.局部变量
Runnable a = ()->{
for (int i = start; i <=end ; i++) {
System.out.println(i);
}
};
因为接收变量的类型是Lambda接口,所以Lambda表达式才知道要实现的是Runnable 这个接口。
2.形式参数
new Thread(()->{
for (int i = start; i <=end ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}).start();
我们看看Thread方法的源码。
Runnable 是形式参数,也就是说通过形式参数,知道了写的Lambda表达式是Runnable 接口的实现。
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}