Java入门级教程13-多线程同步安全机制synchronized(内置锁)、JavaMail发送电子邮箱、爬取CSDN到邮箱、备份数据库
目录
1.Java多线程同步安全机制——synchronized(内置锁)
1.1 synchronized 对同一个资源加锁才能锁得住
1.Java多线程同步安全机制——synchronized(内置锁)
通过加锁机制保证同一时刻只有一个线程能执行特定代码,从而避免多个线程同时操作共享资源导致的数据不一致
synchronized又叫同步锁,暗锁和自动释放锁
1.1 synchronized 对同一个资源加锁才能锁得住
1.1.1 对不同资源
当多线程操作不同对象实例的synchronized方法时,因锁对象不同,线程间无竞争,同步失效(可并行执行)
package com.hy.chapter8;
public class UserThread extends Thread {
public synchronized void run() {
for (int i = 0; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ",执行" + i);
}
}
public static void main(String[] args) {
UserThread u1 = new UserThread(); // 实例1:锁对象是u1
UserThread u2 = new UserThread(); // 实例2:锁对象是u2
u1.start(); // 线程1持有的锁是u1
u2.start(); // 线程2持有的锁是u2
}
}
输出结果:可能乱序
Thread-1,执行0
Thread-1,执行1
Thread-1,执行2
Thread-1,执行3
Thread-0,执行0
Thread-0,执行1
Thread-0,执行2
Thread-0,执行3
Thread-0,执行4
Thread-0,执行5
Thread-1,执行4
Thread-1,执行5
1.1.2 对同一个资源
当多线程操作同一个对象实例的synchronized方法时,锁机制生效,线程会排队执行(互斥)
package com.hy.chapter8;
public class UserThread extends Thread {
public synchronized void run() {
for (int i = 0; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ",执行" + i);
}
}
public static void main(String[] args) {
UserThread u1 = new UserThread(); // 唯一实例:锁对象是u1
Thread t1 = new Thread(u1); // 线程1基于u1创建,执行u1的run方法
Thread t2 = new Thread(u1); // 线程2基于u1创建,执行u1的run方法
t1.start();
t2.start();
}
}
输出结果:顺序固定
Thread-1,执行0
Thread-1,执行1
Thread-1,执行2
Thread-1,执行3
Thread-1,执行4
Thread-1,执行5
Thread-2,执行0
Thread-2,执行1
Thread-2,执行2
Thread-2,执行3
Thread-2,执行4
Thread-2,执行5
1.2 synchronized关键字用于单例模式创建对象
多线程环境下的懒汉式单例模式实现,核心是通过synchronized关键字保证单例对象的唯一性
1.2.1 未使用synchronized关键字
package com.hy.chapter9;
class User {
private static User u;
private User() {}
public static User getInstance() {
if (null == u) {
System.out.println(Thread.currentThread().getName() + "创建对象");
u = new User();
}
return u;
}
}
class UserThread1 extends Thread {
public void run() {
User u1 = User.getInstance();
System.out.println(u1);
}
}
class UserThread2 extends Thread {
public void run() {
User u2 = User.getInstance();
System.out.println(u2);
}
}
public class Test {
public static void main(String[] args) {
UserThread1 u1 = new UserThread1();
UserThread2 u2 = new UserThread2();
u1.start();
u2.start();
}
}
输出结果:
Thread-0创建对象
Thread-1创建对象
com.hy.chapter9.User@370720d1
com.hy.chapter9.User@552a72f3
1.2.2 使用synchronized关键字
package com.hy.chapter9;
class User {
private static User u;
private User() {}
// 懒汉式单例,用到对象才创建
public synchronized static User getInstance() {
if (u == null) {
System.out.println(Thread.currentThread().getName() + "创建对象");
u = new User();
}
return u;
}
}
class UserThread1 extends Thread {
public void run() {
User u1 = User.getInstance();
System.out.println(u1);
}
}
class UserThread2 extends Thread {
public void run() {
User u2 = User.getInstance();
System.out.println(u2);
}
}
public class Test {
public static void main(String[] args) {
UserThread1 u1 = new UserThread1();
UserThread2 u2 = new UserThread2();
u1.start();
u2.start();
}
}
输出结果:
package com.hy.chapter7;
public class Test {
public static void main(String[] args) {
UserRunable ur = new UserRunable();
Thread t1 = new Thread(ur);
Thread t2 = new Thread(ur);
t1.start();
t2.start();
}
}
/**
*输出结果:
*Thread-1,执行0......Thread-1,执行10
*Thread-0,执行0......Thread-0,执行10
*
*或者:
*Thread-0,执行0......Thread-0,执行10
*Thread-1,执行0......Thread-1,执行10
*
*/
Thread-0创建对象
com.hy.chapter9.User@5b74b597
com.hy.chapter9.User@5b74b597
1.3 余额变化
1.3.1 未使用synchronized关键字
代码若未加同步锁,多线程并发修改共享资源money时,会出现数据不一致(结果不可控),存在线程安全问题。
package com.hy.chapter10;
// synchronized同步,就是加锁的操作,保证多线程竞争同一个资源时的安全。
// 单例模式,在多线程情况下,会创建多个对象,怎么保证单例对象,synchronized
// 银行类
class Bank {
// 卡号
private String bankNumber = "";
// 账户的金额
private double money = 0.0;
public Bank(String bankNumber, double money) {
this.bankNumber = bankNumber;
this.money = money;
}
// 操作银行账户的方法
public void operatorBank(double operatorMoney) {
this.money += operatorMoney;
System.out.println(Thread.currentThread().getName() + ",操作的金额是:" + operatorMoney + ",现在账户剩余的金额是:" + this.money);
}
}
class JindongThread extends Thread {
double opMoney;
Bank bank;
public JindongThread(String threadName, double opMoney, Bank bank) {
super(threadName);
this.opMoney = opMoney;
this.bank = bank;
}
public void run() {
this.bank.operatorBank(this.opMoney);
}
}
class WeixinThread extends Thread {
double opMoney;
Bank bank;
public WeixinThread(String threadName, double opMoney, Bank bank) {
super(threadName);
this.opMoney = opMoney;
this.bank = bank;
}
public void run() {
this.bank.operatorBank(this.opMoney);
}
}
class ZhifubaoThread extends Thread {
double opMoney;
Bank bank;
public ZhifubaoThread(String threadName, double opMoney, Bank bank) {
super(threadName);
this.opMoney = opMoney;
this.bank = bank;
}
public void run() {
this.bank.operatorBank(this.opMoney);
}
}
public class Test {
public static void main(String[] args) {
Bank bank = new Bank("10086", 1000.0);
ZhifubaoThread z = new ZhifubaoThread("支付宝", 300, bank);
WeixinThread w = new WeixinThread("微信", -400, bank);
JindongThread j = new JindongThread("京东", 600, bank);
z.start();
w.start();
j.start();
}
}
输出结果:
支付宝,操作的金额是:300.0,现在账户剩余的金额是:1500.0
京东,操作的金额是:600.0,现在账户剩余的金额是:1500.0
微信,操作的金额是:-400.0,现在账户剩余的金额是:1500.0
1.3.2 使用synchronized关键字
代码通过synchronized修饰操作方法,保证了同一时间只有一个线程能修改账户金额,解决了线程安全问题,结果始终正确。
package com.hy.chapter10;
//银行类
class Bank {
// 卡号
private String bankNumber = "";
// 账户的金额
private double money = 0.0;
public Bank(String bankNumber, double money) {
this.bankNumber = bankNumber;
this.money = money;
}
// 操作银行账户的方法
public synchronized void operatorBank(double operatorMoney) {
this.money += operatorMoney;
System.out.println(Thread.currentThread().getName() + ",操作的金额是:" + operatorMoney + ",现在账户剩余的金额是:" + this.money);
}
}
class JindongThread extends Thread {
double opMoney;
Bank bank;
public JindongThread(String threadName, double opMoney, Bank bank) {
super(threadName);
this.opMoney = opMoney;
this.bank = bank;
}
public void run() {
this.bank.operatorBank(this.opMoney);
}
}
class WeixinThread extends Thread {
double opMoney;
Bank bank;
public WeixinThread(String threadName, double opMoney, Bank bank) {
super(threadName);
this.opMoney = opMoney;
this.bank = bank;
}
public void run() {
this.bank.operatorBank(this.opMoney);
}
}
class ZhifubaoThread extends Thread {
double opMoney;
Bank bank;
public ZhifubaoThread(String threadName, double opMoney, Bank bank) {
super(threadName);
this.opMoney = opMoney;
this.bank = bank;
}
public void run() {
this.bank.operatorBank(this.opMoney);
}
}
public class Test {
public static void main(String[] args) {
Bank bank = new Bank("10086", 1000.0);
ZhifubaoThread z = new ZhifubaoThread("支付宝", 300, bank);
WeixinThread w = new WeixinThread("微信", -400, bank);
JindongThread j = new JindongThread("京东", 600, bank);
z.start();
w.start();
j.start();
}
}
输出结果:
支付宝,操作的金额是:300.0,现在账户剩余的金额是:1300.0
京东,操作的金额是:600.0,现在账户剩余的金额是:1900.0
微信,操作的金额是:-400.0,现在账户剩余的金额是:1500.0
1.3.3 使用synchronized同步块
在 1.2.5 的基础上优化了Bank类的实现,使用Synchronized同步块,对需要竞争的代码进行锁定,降低锁定的范围,优化性能。
package com.hy.chapter11;
//银行类
class Bank {
// 卡号
private String bankNumber = "";
// 账户的金额
private double money = 0.0;
public Bank(String bankNumber, double money) {
this.bankNumber = bankNumber;
this.money = money;
}
// 操作银行账户的方法 synchronized 修饰方法,会给整个方法加锁,导致整个方法被锁定,导致锁定的范围过大
// synchronized 同步块,对需要竞争的代码进行锁定,降低锁定的范围,优化性能
public void operatorBank(double operatorMoney) {
System.out.println("欢迎您到银行办理具体的业务");
synchronized (Bank.class) { // ()里面一定是引用类型对象,必须是同一个对象
this.money += operatorMoney;
System.out.println(
Thread.currentThread().getName() + ",操作的金额是:" + operatorMoney + ",现在账户剩余的金额是:" + this.money);
}
System.out.println("谢谢您,欢迎下次光临");
}
}
// 其他类代码保持不变
输出结果:
欢迎您到银行办理具体的业务
欢迎您到银行办理具体的业务
欢迎您到银行办理具体的业务
支付宝,操作的金额是:300.0,现在账户剩余的金额是:1300.0
谢谢您,欢迎下次光临
京东,操作的金额是:600.0,现在账户剩余的金额是:1900.0
谢谢您,欢迎下次光临
微信,操作的金额是:-400.0,现在账户剩余的金额是:1500.0
谢谢您,欢迎下次光临
注意:在Synchronized( )中,( )里面一定是引用类型对象,且必须是同一个对象,例如this, bankNumber, Bank.class
1.4 票数变化
1.4.1 未使用synchronized同步块
无同步时,多线程并发操作共享变量会导致数据混乱(线程不安全)
package com.hy.chapter15;
public class Buy implements Runnable {
// 票数
private int sum = 10;
private boolean flag = true;
@Override
public void run() {
while (flag) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + ",买到了票,是第" + this.sum-- + "张票");
if (this.sum <= 1) {
this.flag = false;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Buy b = new Buy();
new Thread(b, "张三线程").start();
new Thread(b, "李四线程").start();
new Thread(b, "王五线程").start();
}
}
输出结果:

1.4.2 使用synchronized同步块
有同步时,通过互斥锁保证关键操作的原子性,解决核心线程安全问题
package com.hy.chapter15;
public class Buy implements Runnable {
// 票数
private int sum = 10;
private boolean flag = true;
@Override
public void run() {
while (flag) {
try {
Thread.sleep(1000);
synchronized (this) {
System.out.println(Thread.currentThread().getName() + ",买到了票,是第" + this.sum-- + "张票");
}
if (this.sum <= 1) {
this.flag = false;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Buy b = new Buy();
new Thread(b, "张三线程").start();
new Thread(b, "李四线程").start();
new Thread(b, "王五线程").start();
}
}
输出结果:

2.使用JavaMail发送电子邮箱📮
2.1 加载Maven依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.38</version>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
2.2 QQ邮箱授权
2.2.1 老版QQ邮箱授权
进入设置 -->账号 -->服务状态开启服务 -->点击继续获取授权码 -->微信扫码发送短信 -->发送成功后点击“我已发送”获取授权码
2.2.2 新版QQ邮箱授权
① 首页找到设置,进入设置,点击账号与安全

② 在安全设置中,找到POP3...服务,点击"开启服务"后,按照要求用微信扫码发送短信

③ 短信发送成功后,点击“我已发送”获取授权码

2.3 具体实现步骤
2.3.1 创建MailRunnable类
package com.hy.chapter12;
import java.security.GeneralSecurityException;
import com.sun.mail.util.MailSSLSocketFactory;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.extra.mail.MailAccount;
import cn.hutool.extra.mail.MailUtil;
public class MailRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "准备发送邮件....");
try {
// 构建邮件账户配置对象
MailAccount mailAccount = new MailAccount();
// 配置SSL加密(关键:QQ邮箱SMTP服务要求SSL连接)
mailAccount.setSslEnable(true); // 启用SSL
MailSSLSocketFactory mss = new MailSSLSocketFactory("TLSv1.2"); // 设置TLS版本(兼容多数邮箱)
mss.setTrustAllHosts(true); // 信任所有主机(避免证书验证失败,开发环境常用)
// 配置SMTP服务器信息(以QQ邮箱为例)
mailAccount.setHost("smtp.qq.com"); // QQ邮箱SMTP服务器地址
mailAccount.setPort(465); // SSL连接端口(QQ邮箱常用465或587)
mailAccount.setAuth(true); // 启用身份验证(必须开启)
// 配置发件人信息
mailAccount.setFrom("你的QQ账号@qq.com"); // 发件人邮箱(需与下面的user一致)
mailAccount.setUser("你的QQ账号"); // 发件人账号(通常是邮箱前缀,如QQ号)
mailAccount.setPass("你的临时授权密码"); // 发件人授权码(非QQ登录密码,需单独获取)
// 发送邮件 单发
// 参数:账户配置、收件人邮箱、邮件主题、邮件内容、是否HTML格式
MailUtil.send(mailAccount, "QQ账号1@qq.com", "java发送邮件", "你好让我们一起学习Java", false);
// 群发
//MailUtil.send(mailAccount,
// CollUtil.newArrayList("QQ账号1@qq.com",
// "QQ账号2@qq.com",
// "QQ账号3@qq.com"),
// "java发送邮件机制",
// "你好让我们一起学习Java", false);
// 发送带附件的邮件
// 最后一个参数是附件文件(可传入多个File对象)
//MailUtil.send(mailAccount,
// CollUtil.newArrayList("QQ账号1@qq.com"),
// "java发送带附件的邮件",
// "<h1>这个是html内容的邮件信息</h1>", // HTML格式内容(需将最后一个参数设为true)
// true,
// FileUtil.file("d:/cool.png") // 附件文件(Hutool的FileUtil简化文件操作)
//);
System.out.println("发送邮件成功....");
} catch (GeneralSecurityException e) {
e.printStackTrace();
}
}
}
2.3.2 创建测试类
package com.hy.chapter12;
public class Test {
public static void main(String[] args) {
MailRunnable m = new MailRunnable(); // 创建邮件任务
Thread t = new Thread(m); // 将任务交给线程
t.start(); // 启动线程(异步执行邮件发送)
}
}
输出结果:
Thread-0准备发送邮件....
发送邮件成功....
2.3.3 验证邮件是否发送成功
邮件发送成功,在所写接受邮件账号的QQ邮箱中查看邮件

3.爬取CSDN主页博客信息,并发送到邮箱
3.1 爬取CSDN主页博客信息
CsdnTask:继承TimerTask,作为定时任务的具体实现 ——类加载时爬取一次 CSDN 指定博客列表页面,之后每次定时任务触发时,从爬取的结果中提取一条博客数据(标题、链接、内容等)
package com.hy.chapter13;
import java.util.TimerTask;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
public class CsdnTask extends TimerTask {
static int i = 1;
static Elements elements;
static {
try {
String url = "https://blog.csdn.net/weixin_74137141/article/list/1";
// 1.第一步要求判断能不能去爬取这个链接
Connection conn = Jsoup.connect(url);
System.out.println(conn);
// 2.获取这个文档对象
Document doc = conn.get();
// System.out.println(doc);
// 3.获取整合文档doc
elements = doc.select(".article-list .article-item-box");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
Element e = elements.get(i);
String title = e.select("a").text();
String link = e.select("a").attr("href");
String content = e.select(".content").text();
String date = e.select(".date").text();
String num = e.select(".read-num").first().text();
System.out.println("一条博客的信息为:" + title + "," + link + "," + content + "," + date + "," + num);
// 多线程发邮件
MailRunnable m = new MailRunnable(title, link, content, date, num);
new Thread(m).start();
// 可以实现写入数据库。。
i++;
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.2 邮件发送任务
MailRunnable:实现Runnable,作为邮件发送任务 —— 接收CsdnTask传递的博客数据,通过 Hutool 配置 QQ 邮箱,将数据以 HTML 格式 + 附件的形式发送到指定收件人邮箱
package com.hy.chapter13;
import java.security.GeneralSecurityException;
import com.sun.mail.util.MailSSLSocketFactory;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.extra.mail.MailAccount;
import cn.hutool.extra.mail.MailUtil;
public class MailRunnable implements Runnable {
String title;
String link;
String content;
String date;
String num;
public MailRunnable(String title, String link, String content, String date, String num) {
this.title = title;
this.link = link;
this.content = content;
this.date = date;
this.num = num;
}
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName() + "准备发送邮件....");
try {
// 构建一个邮件的MailAccount对象
MailAccount mailAccount = new MailAccount();
mailAccount.setSslEnable(true);
MailSSLSocketFactory mss = new MailSSLSocketFactory("TLSv1.2");
// 信任所有的host
mss.setTrustAllHosts(true);
// 设置邮件的配置信息
mailAccount.setHost("smtp.qq.com");
mailAccount.setPort(465);
mailAccount.setAuth(true);
mailAccount.setFrom("你的QQ账号@qq.com");
mailAccount.setUser("你的QQ账号");
mailAccount.setPass("你的临时授权密码");
MailUtil.send(mailAccount, CollUtil.newArrayList("QQ账号1@qq.com"), "csdn算法与编程之美的博客系列",
"<p>标题为:" + this.title + "</p>" + "<p>链接为:" + this.link + "</p>内容为:" + "<p>" + this.content + "</p>"
+ "<p>发布时间为:" + this.date + "</p>",
true, FileUtil.file("d:/cool.png"));
System.out.println("发送邮件成功....");
} catch (GeneralSecurityException e) {
e.printStackTrace();
}
}
}
3.3 创建定时器,定时发送邮件
Test类中的代码是整个 “定时爬取数据并发送邮件” 系统的入口程序,核心作用是通过Timer(Java 定时任务工具)配置并启动CsdnTask任务,实现 “延迟指定时间后开始,按随机周期重复执行爬取和邮件发送” 的自动化流程。
package com.hy.chapter13;
import java.util.Random;
import java.util.Timer;
//用一个线程每隔一段时间爬取一条数据,并给你的邮箱发送这条数据
public class Test {
public static void main(String[] args) {
// 建立一个定时器
Timer t = new Timer();
// 1 秒后启动任务,并以 0~80 秒的随机固定周期重复执行
t.schedule(new CsdnTask(), 1 * 1000, new Random().nextInt(80000));
}
}
输出结果:

邮箱查询:

详细内容:

4.每天备份数据库文件
基于 Java 实现的 MySQL 数据库备份工具,核心逻辑是通过 Java 调用系统的mysqldump.exe(MySQL 官方备份工具),将指定数据库的内容导出为 SQL 文件并保存到本地
正常查找MySQL 官方备份工具路径:(注意MySQL版本)
C: >program Files >MySQL >MySQL Server 5.7 >bin >mysqldump.exe
package com.hy.chapter14;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
public class Test1 {
public static void main(String[] args) {
// 1. 初始化IO流对象( BufferedReader:读备份内容;PrintWriter:写SQL文件)
BufferedReader br = null;
PrintWriter pw = null;
// 2. 数据库备份参数(IP、用户名、密码、数据库名)
String ip = "127.0.0.1";
String username = "root";
String userpwd = "yourpassword";
String databaseName = "mysql2025";
try {
// 调用Runtime.exec()启动mysqldump.exe进程,执行备份命令
//Process p1 = Runtime.getRuntime().exec(
// "C:\\Program Files\\MySQL\\MySQL Server 5.7\\bin\\mysqldump.exe -h127.0.0.1 -uroot -pyourpassword mysql2025");
// IP和用户名和密码后面值不要由空格
Process p1 = Runtime.getRuntime().exec("C:\\Program Files\\MySQL\\MySQL Server 5.7\\bin\\mysqldump.exe -h"
+ ip + " -u" + username + " -p" + userpwd + " " + databaseName);
// 3.1 构建输入流:读取mysqldump进程输出的SQL内容(字节流→字符流→缓冲流,提高效率)
br = new BufferedReader(new InputStreamReader(p1.getInputStream(), "UTF-8"));
// 3.2 构建输出流:将SQL内容写入D盘的mysql2025.sql文件
pw = new PrintWriter(new File("d:/mysql2025.sql"));
// 3.3 逐行读取SQL内容,写入文件
String line = "";
while ((line = br.readLine()) != null) { // 读取一行内容,直到流末尾(null)
pw.println(line); // 将一行SQL写入文件
}
pw.flush(); // 强制刷新缓冲区:确保所有内容写入文件(避免缓冲区残留数据丢失)
// waitFor():阻塞当前Java线程,等待mysqldump进程执行完毕
// 进程退出码为0 → 正常执行;非0 → 执行失败(如密码错误、数据库不存在)
if (p1.waitFor() == 0) {
System.out.println("备份数据库正常退出");
}
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭BufferedReader(输入流)
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 关闭PrintWriter(输出流)
if (pw != null) {
pw.close(); // PrintWriter的close()会自动刷新缓冲区,此处可省略额外flush()
}
}
}
}
注意:数据库密码、表名称等相关信息要改成自己的
输出结果:
备份数据库正常退出
输出sql文件内容:

更多推荐


所有评论(0)